Repository navigation
Release v1.4.0 - #663
Release v1.4.0#663
Conversation
…vas-styles filters Two extension points third-party packages need to decorate rendered output without editing the visual-editor package. - `ap.visual-editor.rendered-block` (PHP) fires in `BlockRenderer::renderBlock` after each block (static or dynamic) finishes rendering. Callbacks receive the HTML, the block name, and the normalized attributes, and must return an HTML string. Lets cross-cutting effects (frosted glass, contrast overlays, motion wrappers) post-process a block's markup without every host having to modify the renderer. - `ap.visual-editor.canvas-styles` (JS) filters the ordered `CanvasStyle[]` handed to `BlockCanvas`'s `__unstableEditorStyles` prop. Callbacks return a `CanvasStyle[]`; non-array returns fall through to the base list and non-object entries are dropped. Lets packages push their stylesheets into the Gutenberg iframe (which is sandboxed from the parent document's Vite-injected styles) without touching `canvas-styles.ts` for every new integration. Motivated by the artisanpack-ui/liquid-glass integration — the package now uses both filters to (1) decorate dynamic-block HTML with the frosted-glass class + CSS vars and (2) inject its stylesheet into the iframe canvas. No behavioral change for hosts that don't hook them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eir contracts Follow-up to b1a8fa0. A code-review pass on the filter feature surfaced that neither extension point had test coverage and that three real implementer footguns were undocumented. This commit closes both gaps: Docs - `BlockRenderer::renderBlock`: call out the RECURSION behavior (the filter fires once per block at every level of the tree, so a container-wrapping callback also wraps every descendant unless it branches on `$name`/`$attributes`) and the ATTRIBUTES SHAPE contract (the array already carries the internal `_resolved*` keys stamped by `stampSiteMeta` / `stampLoginout`, which are a package-internal side channel, not stable public attributes). - `canvas-styles.ts`: fold the stranded pre-existing docblock into a single `baseCanvasStyles` block, and add a TIMING callout to the `canvasStyles` export — the IIFE runs `applyFilters` synchronously at module-load, so callbacks registered after the first import silently no-op. Tests - `BlockRendererTest`: new `describe('ap.visual-editor.rendered-block filter')` covering static wrap, dynamic wrap, per-level recursion order (`[paragraph, paragraph, group]`), non-string-return discard, stamped-attribute pass-through, and no-op with zero callbacks. - `canvas-styles.filter.test.ts`: new file, mocks `@wordpress/hooks` per the sibling `fork-class-name-alias.test.ts` pattern and covers the callback receiving a mutable copy of the base list, appended entries landing in the export, non-array fallback to base, and drop-on-shape-mismatch for both non-string `css` and non-object entries. Verified: pest BlockRendererTest → 89 passed (was 83, +6); vitest editor/__tests__ → 252 passed across 28 files (was 247, +5). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lter feat: rendered-block + canvas-styles extension filters
Adds a shared JS filter that lets external packages contribute panels to the background/appearance area of any block that opts into a background support, without each package enumerating target blocks and wrapping editor.BlockEdit itself. The visual editor now owns the target-block decision via a single editor.BlockEdit HOC that gates on supports.background and supports.color.background, then renders filter-registered controls sorted by priority (default 10, lower first) and deduped by id (last-wins). Downstream packages like liquid-glass can drop their per-package withBlockEdit HOC and register a single filter callback instead. Closes #649
- Register the BlockEdit HOC BEFORE responsive/state HOCs so it wraps innermost. context.attributes is now the breakpoint-merged / state-resolved view (not raw idle) and context.setAttributes routes writes through the responsive/state wrappers. - Wire registerBackgroundControls into the site-editor bootstrap so the filter fires when editing templates, template parts, and patterns (previously dead in the site editor). - Dedupe by id BEFORE priority sort so "last-wins mirroring @wordpress/hooks" tracks registration order, not sort order — a later addFilter overrides an earlier one at the same id regardless of relative priorities. - Deep-clone context.blockSupports (structuredClone) so a filter callback can't corrupt @wordpress/blocks' registry via mutation. Freeze context.attributes and type both fields as Readonly<>. - Wrap applyFilters in try/catch; log to console.error and return an empty list on throw so one bad third-party filter can't trip BlockCrashBoundary for every block in the session. - Reject controls with a non-finite priority (NaN, strings, Infinity) at validation time — engine-dependent sort order eliminated. - Reject empty-string id at validation time. - Always return the Fragment shape from the HOC — a 0→1 controls transition no longer remounts BlockEdit and lose RichText cursor / drag / child hook state. - Drop the dead `data-background-control` wrapper div. - Move `ap.visual-editor.rendered-block` docs to the PHP filters section with correct description (it's a server-side PHP filter, not a save-time JS filter). - Fix the docs example gate to check both `blockSupports.background` and `blockSupports.color.background` — the naive `!blockSupports.background` check would silently no-op on color-background blocks. Overlaps with CodeRabbit review comments on PR #652 (priority type-check + resolveBlockSupports live-reference leak). New tests cover: registration-order-over-priority dedupe (both directions), NaN/string/Infinity priority rejection, empty-id rejection, thrown-callback swallowing, attributes-freeze semantics, blockSupports deep-clone semantics, always-Fragment BlockEdit stability across empty→non-empty controls transitions. Test count: 20 → 27 in background-controls; full editor suite 1861 → 1868 passing (site-editor-app.test.tsx pre-existing failure remains, now surfacing as a transitive `@wordpress/blocks` JSON-import error during collection rather than the previous 17 runtime failures — the file has not passed on release/1.x for this reason). Refs #649
The new site-editor bootstrap import of `registerBackgroundControls` pulls the background-controls barrel into the site-editor-app test's module graph, which transitively loads `@wordpress/block-editor` and its `block-compare` → `diff` subpath — the same unresolved-under-jsdom chain the file already stubs `BlockEditorBoundary` and `BlockLibrarySidebar` for. Stub `../../background-controls` to a no-op registrar so the shell test can boot again on CI. The HOC and filter helpers have their own focused suites under `background-controls/__tests__/`. Refs #649
…ntrols-filter feat: ap.visual-editor.background-controls filter (#649)
Extends the ViewportSwitcher into a unified device-preview + edit-scope control. Selecting a preset atomically resizes the canvas iframe to the breakpoint's `previewWidthPx` and scopes subsequent style edits to that key. The `base` button previews at full editor width and scopes edits to the cascade root. Registry shape (both TS and PHP): - Each `Breakpoint` gains an optional `label` (display string) and `previewWidthPx` (canvas iframe width) alongside `minWidthPx`. - PHP config accepts scalar and object form: `'sm' => '640px'` (back- compat) or `'sm' => ['minWidthPx' => 640, 'previewWidthPx' => 375, 'label' => 'iPhone']`. Partial objects merge into the default at the same key. Scalar overrides preserve default `label`/`previewWidthPx` because `mergeByKey` normalises each layer to a fragment before merging. Ship defaults: Mobile (sm/375px), Tablet (md/768px), Desktop (lg/1440px), xl+ (1280px), 2xl+ (1536px). The `+` suffix is kept on the two size-only keys to preserve the mobile-first cascade signal from #487. Bootstrap bridge: both Blade templates (post editor's `x-visual-editor` component and the site editor's `index.blade.php`) stamp `data-breakpoints` with the merged `BreakpointRegistry` snapshot. The React shells parse it, hydrate via `registryFromSnapshot()`, and pass the resulting registry to `TopBar` as `viewportRegistry`. Falls back to ship defaults when a host omits the payload. Wiring: - Post editor: shared `useCanvasPreviewWidth()` hook + inline `width` on `.ap-visual-editor__canvas-frame`; parent `.ap-visual-editor__canvas` gets `overflow-x: auto` so wide presets scroll horizontally in narrow editors per the issue spec. - Site editor: same hook + `siteEditorCanvasPreviewProps()` helper spread on the entity, lazy, and empty-state canvas branches; CSS applies the fixed width to the direct child inside the scrollable canvas container. - `CanvasFrame` empty state now honours the active preset so the width slot stays consistent across all three canvas branches. - `InspectorScopeChip` reads labels from an optional registry prop and falls back to a `TAILWIND_V4_DEFAULTS`-built registry so the chip and switcher never disagree on the shipped labels. Tests: - Pest: BreakpointRegistry — object-form validation, partial-object merges, scalar-preserves-defaults regressions, `toArray()` wire shape. - Vitest: `TAILWIND_V4_DEFAULTS` labels + preview widths, hydration back-compat, `ViewportSwitcher` label rendering + `onChange` payload, post-editor `EditorCanvas` inline width, `TopBar` viewport wiring end-to-end, site-editor preset applies data-preview-width. Docs: `docs/blocks/Responsive-Design-Tools.md` updated with the object- form config example, ship defaults table, and unified control model. Config file docblock updated with the same. Closes #617 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e-preview-device-sizes feat: responsive preview — device sizes in visual + site editor (#617)
Adds a per-block Position inspector panel (dropdown for static/relative/absolute/fixed/sticky, per-side offset inputs with px/%/rem/em/vh/vw/auto units, z-index, per-breakpoint overrides via the responsive tab pattern from #487) plus scoped CSS emission in both the editor canvas and the Blade renderer's frontend output. Opt-in per block via `supports.position: true` (Gutenberg's native `{ sticky: true }` object shape also counts). Legacy `style.position: "sticky"` strings widen into the structured schema — no migration. ## Structure - `src/Position/` — PHP resolver + emitter mirroring the JS shape for the Blade renderer path. - `resources/js/visual-editor/positioning/` — JS resolver, emitter, supports extension, inspector control (with #644 positioned-ancestor warning), style HOC (`_positionScopeId` mint/claim + `ve-pos-<id>` class + `<style>` element + inline base fallback), + tests. - `packages/visual-editor-renderer-blade/src/Services/PositionCssAccumulator.php` — per-request rule accumulator; the `<x-ve-blocks>` / `<x-ve-template>` components drain it into a single `<style data-ve-position>` block. ## Notable resilience choices - Base declarations stamp inline on the wrapper (both JS `blocks.getSaveContent.extraProps` and PHP `BlockSupports::compile`) so hosts that strip `<style>` from block markup or CSP-block inline `<style>` elements still get sticky/absolute applied. - All declarations use `!important` to beat Gutenberg's editor-canvas internal `.block-editor-block-list__block { position: relative }` rule and theme resets that use `!important` on wrapper classes. - `PositionCssAccumulator::push` keys by `scope + hash(rules)` so a `_positionScopeId` collision (paste races, legacy content) keeps both entries instead of silently dropping one. - `PositionResolver::normalizeZIndex` rejects scientific notation strings — PHP `is_numeric` accepts `'1e100'` and overflows to PHP_INT_MAX; the JS side filters via `Number.isFinite`. Matching keeps preview and server render byte-identical. ## Consolidation carried on top - `BlockSupports::pushToAccumulator()` — collapses the five copy-pasted `pushResponsive/pushStates/pushGradientBorder/pushBoxShadow/pushPosition` guards into one helper. - `BlockSupports::composeScopeClass()` — collapses the three copy-pasted `resolve*ScopeClass` implementations into one helper. - `PositionResolver::mergeLayers()` + exported `mergeLayers` from `positioning/resolver.ts` — one source of truth for the layer fallthrough logic, reused by `mergedBreakpointLayers` on both sides. - `PositionResolver::resolve()` is now called once per block per render (was twice — inline stamp + `<style>` rules); the result threads through `self::$positionPayloadCache`. ## Upgrade notes Hosts that previously ran `php artisan vendor:publish --tag=visual-editor-blade-views` must republish with `--force` — the published Blade files shadow the package source and pre-1.4 copies don't include the new `<style data-ve-position>` output block. ## Tests - 34 JS tests (resolver, emitter, supports extension) + 15 PHP tests (resolver, emitter, accumulator collision handling, compile integration). Full suites green: 86 PHP / 34 JS. Closes #641, #642, #643, #644 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ort-schema-ui feat: CSS positioning support for blocks (#640)
, #646, #647, #648) Closes out the #640 umbrella by (1) flipping the position opt-in on every top-level artisanpack block, (2) shipping integration + e2e coverage, and (3) documenting the feature. Also removes the duplicate breakpoint switcher from the Position panel — the top-bar viewport switcher already drives it via the shared active-breakpoint store. ## Changes ### #645 — 82 artisanpack blocks opt in Bulk-added `"position": true` to `supports` in every `resources/js/visual-editor/blocks/*/block.json` that doesn't declare a `parent` or `ancestor` restriction. Twenty child-only blocks (accordion internals, list items, `column` inside `columns`, `button` inside `buttons`, `post-template` inside `query`, pagination pieces) are deliberately skipped — positioning a child that a container relies on for layout breaks the container's contract. `artisanpack/group` already carried Gutenberg's `supports.position: { sticky: true }` object shape; `positionEnabled()` accepts both `true` and any truthy object, so it was left alone. ### #646 — no-op given the I7 cutover This repo replaces `registerCoreBlocks()` with `registerArtisanPackBlocks()` (I7 · #415). No `core/*` block enters the editor, so a filter injecting position support into core containers would be dead code. Every container listed in the parent issue (`core/group`, `core/columns`, `core/column`, `core/cover`, `core/row`, `core/stack`, `core/grid`, `core/buttons`, `core/image`) has an `artisanpack/*` fork covered by #645. ### #647 — integration + e2e coverage - **Integration (Vitest, real):** `resources/js/visual-editor/positioning/__tests__/integration.test.ts` round-trips the full resolver → merger → emitter chain across all five Tailwind breakpoints, verifies raw vs. effective readers, legacy sticky no-churn, and the static-toggle-preserves-orphan-fields flow. 4 new tests, 38 positioning tests total green. - **E2E (Playwright shape, commented):** `tests/E2E/positioning.spec.ts` documents five runtime scenarios — sticky group on canvas scroll, absolute cover inside a positioned parent, ancestor warning when the parent is static, per-breakpoint viewport switching, legacy sticky frontend render. Follows the `animations.spec.ts` precedent — activated when the Playwright runner lands. ### #648 — docs New `docs/position.md` covering opt-in, attribute shape, position values, offset units, per-breakpoint inheritance, the ancestor warning, the two frontend emission channels (inline + `<style data-ve-position>`), and a troubleshooting section. Linked from `docs/home.md`. ### Panel cleanup — remove duplicate breakpoint switcher The Position panel had its own `<Base | Mobile | Tablet | Desktop | xl+ | 2xl+>` tab strip that flipped the shared active-breakpoint store. The top-bar viewport switcher already drives that store — the panel's local tabs were a second control for the same job. Removed the tab strip; the panel reads `activeBreakpoint` via `useSyncExternalStore` and re-renders effective values / "Inherited" hints when the top-bar switches. Docs updated to note the panel follows the top-bar switcher. ## Tests - 38 positioning JS tests (34 unit + 4 new integration) — green. - Full JS suite: 2809 passed / 18 pre-existing failures (site-editor `localStorage` under jsdom, unrelated). No new regressions from the 82 block.json edits. Closes #645, #646, #647, #648 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Applies the 10 findings from the local /code-review pass on the initial commit of this PR. No behavior changes to end users beyond the tab-strip removal from the previous commit (already reviewed and approved). ## Correctness - **JSON reformatting churn on 17 block.json files (Angle A #2, Conv #2).** Reverted every touched `block.json` to the base branch state, then re-inserted `"position": true,` via a text-level regex that only adds one line. Diff footprint for the block manifests dropped from 82 files / 695 insertions / 82 deletions to 82 files / 82 insertions — one clean line per manifest. Preserves the original compact inline arrays (`"keywords": [ "a", "b" ]`, `"align": [ "wide", "full" ]`, `"enum": [ ... ]`) and keeps git-blame on those arrays pointed at the original authors instead of this PR. - **Stale JSDoc (Angle A #1, Angle B #1).** The `with-position-control.tsx` docblock still described the removed tab strip. Rewrote to reflect the actual behavior: the panel follows the top-bar `ViewportSwitcher` via the shared active-breakpoint store, matching every sibling responsive-aware inspector panel. - **CHANGELOG path case (Angle A #3, Conv #1).** `tests/e2e/positioning.spec.ts` → `tests/E2E/positioning.spec.ts` — git tracks the file at the uppercase path (matches the `animations.spec.ts` sibling). macOS's case-insensitive filesystem hid the mismatch locally. - **CHANGELOG missing UX note (Angle B #5).** Added a sentence explaining that per-breakpoint authoring follows the top-bar viewport switcher and no separate control lives in the panel. Answers the anticipated support ticket "where did the Base/Mobile/Tablet/Desktop buttons in the Position panel go?". - **Panel-vs-top-bar dependency (Angle A #4, Angle B #3).** Verified intentional and consistent with sibling panels; documented in the new CHANGELOG sentence and the rewritten JSDoc. No code change — the a11y story is handled at the inspector level by `InspectorScopeChip` (`role="status" aria-live="polite"`). ## Cleanup - **Dead `PositionEmitter` DI binding (Angle C #4).** Removed the scoped binding from `VisualEditorServiceProvider.php` plus its `use` import. `BlockSupports::compilePosition()` instantiates the emitter directly with the request-scoped `BreakpointRegistry` — no container round-trip needed. - **`docs/position.md` Related links (Angle A #6).** Switched from relative Markdown `[Animations](animations.md)` to wiki-style `[[Animations]]`, matching `docs/home.md`. - **Brittle exact-string assertion (Angle A #5).** Replaced the order-and-whitespace-sensitive `.toBe(...)` with individual `.toContain(...)` checks in `integration.test.ts`, matching the surrounding assertions. - **`docs/home.md` slash spacing (Conv #3).** `(static/relative/...)` → `(static / relative / ...)`, matching the sibling `Linear / radial / conic borders` entry directly above. ## Verified out (not fixed) - **Dynamic blocks miss position CSS on frontend (Angle C #1).** `PositionEmitter` IS reached via `BlockSupports::compilePosition()` → `compile()` → block partials calling `wrapperAttrs()`. The DI binding was dead but the code path is live. - **`addSaveProps` writes string `style` (Angle C #3).** WordPress's `renderToString` (save serialization path) accepts string style attributes; user smoke test confirmed frontend renders correctly. - **`responsive` attribute injection breaks downgrade (Angle C #2).** Real forward-only concern for the feature, not a defect in this PR. ## Tests - 38 positioning JS tests + 86 PHP tests, all green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat: CSS positioning support for blocks (#640)
Adds a WordPress-style "Choose a pattern" modal that auto-opens when a never-saved record is loaded with empty content and can be re-opened at any time from a new top-bar button next to the `+` inserter. - Extends pattern registration with an optional `post_types` scope (Gutenberg convention). Patterns without a scope match every context; patterns with a scope match only when the requested post-type slug is present. Surfaced via new `post_types` field on `PatternAdapter` and a new `?post_type=` query param on `PatternController::index`. - Registers a built-in `page/blank` seed pattern via the `ap.visual-editor.patterns` filter so the modal always has at least one entry to render on a fresh install. - New React module under `editor/page-pattern-modal/`: `use-fresh-content-detection` (empty content + `created_at === updated_at`), `filter-modal-patterns` (strict scope + seed carve-out), and `page-pattern-modal` (grid grouped by primary category, optional template selector, dismiss on X/Escape/backdrop). - `<x-visual-editor>` now stamps microsecond-precision `data-created-at` and `data-updated-at` so the first-save suppression heuristic survives records saved within the same integer second as creation. - Toolbar button + auto-open both suppressed when zero patterns apply or the initial content load hasn't settled — no silent no-op modal. Closes #639 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ern-inserter-modal feat: page pattern inserter modal with template selector (#639)
…491 · #492 · #493) Adds the Block Visibility feature across all three renderers so any block can be conditionally rendered based on runtime context. Server-side evaluation means hidden blocks never emit markup — no flash of content, no privacy leak. Ten rule families ship in this initial pass: Contextual (#491): - Hide (master toggle) - Screen size (CSS-only, per named breakpoint) - Query string (key/value, wildcard "*", Any/All) - Referrer (literal, "*." wildcard, "(direct)") - Browser / OS / Device (bundled UA parser + jenssegers/agent suggestion) User & auth (#492): - Login state (Logged in / Logged out / Either) - User role (Any/All, reads from cms-framework RoleManager with a Laravel Gate + Spatie fallback chain) - Specific user (email- or id-match, autocomplete via new /visual-editor/api/users/search endpoint) Scheduling (#493): - Date/time window (either edge omissible, per-rule timezone) - Recurring weekly schedule (up to 14 windows, DST-safe wall-clock matching) Infrastructure: - `visual-editor:audit-scheduled-blocks` artisan command - `artisanpack.visual-editor.visibility.enabled` site-wide kill switch - `ap.visual-editor.visibility.evaluated` debug hook - `ap.visual-editor.visibility.register-rules` extension filter - `supports.artisanpackVisibility: false` per-block opt-out - `TreePruner` for React/Vue payload serialisation - Shared Inspector "Visibility" panel (subsections only render when active) Tests: - 54 Pest tests (evaluator + each rule + endpoint + TreePruner) - 4 renderer-blade Feature tests (nested-recursion regression guard) - 6 Vitest tests (client-side helpers) - Playwright plan document at tests/E2E/visibility.spec.ts Docs: - docs/visibility.md — editor guide + developer reference - docs/plans/21-…, 22-…, 23-… — plan documents referenced from the issues Deferred to follow-up (documented in the plan docs): - Toolbar Eye badge - Site-editor Preview mode with mocked inputs - Cache auto-tightening for scheduled blocks inside cached template parts - Legacy role-lock migration (no released version persists that shape) Closes #491 Closes #492 Closes #493 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…anel Two issues surfaced during manual verification of the Visibility panel: 1. Toggles for Screen Size, Browser / OS / Device, User Role, Specific User, and Date/Time Window would not stay flipped on. Root cause: `active` was computed from the list length inside the slice (e.g. `active = (breakpoints ?? []).length > 0`), but the seed on toggle-on was an empty list, so `active` re-computed to `false` on the next render and the toggle snapped back off. Fix: derive `active` from slice presence (`value !== undefined`) — the persisted slice itself is the on/off signal. Also relax the parent `patch()` cleaner to keep any defined slice, so an active-but-empty rule stays on the bag. 2. Multi-control subsections were unusable — controls were rendered as flex children of a single `<PanelRow>`, which is designed for one label + one value. Grouped controls (KEY + VALUE + Remove on a query-string clause, DAY + START + END on a recurring window, etc.) collapsed into narrow columns with cramped buttons. Fix: reserve `<PanelRow>` for the top-level toggle only; use flex `<div>` wrappers with proper gaps for grouped controls. Adds a small vertical indent + border-left so each expanded subsection reads as belonging to its toggle. Also tightens the empty-roles case for the User Role rule to hint at what to do rather than render an empty checkbox list. Vitest + Pest suites remain green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two follow-ups from manual verification: 1. Screen Size was effectively "always hidden" at any desktop viewport regardless of which breakpoints the editor checked. Root cause: the Blade + JS emitters produced open-ended `@media (min-width:X)` rules per hidden breakpoint, so checking `sm` hid the block at every viewport ≥ 640px (i.e. all of desktop). The correct semantic for a checkbox UI is "each checkbox is a RANGE": `sm` = 640-767 only, `md` = 768-1023, etc.; only the widest breakpoint stays unbounded. Fix: sort the registered breakpoints ascending by min-width, pair each with the NEXT breakpoint's min-width — 1 as its upper bound, and emit `(min-width:X) and (max-width:Y-1)` per hidden breakpoint. Mirrored on the React and Vue client-side emitters for renderer parity. Blade Feature test + Vitest test updated + expanded. 2. Date/time-window subsection was bulky and misaligned — the `@wordpress/components` `DateTimePicker` renders a full-panel calendar (~450px tall) that's oversized for an inspector row. Swap for a native `<input type="datetime-local">` wrapped in `BaseControl` for consistent labeling. Format is compatible (both sides expect "YYYY-MM-DDTHH:MM") so the persisted value shape is unchanged; a `toLocalIsoFormat()` helper trims any trailing "Z" / `±HH:MM` marker on read since datetime-local rejects those. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Under Semantic A (each checkbox = one specific viewport range), a direction toggle produced surprising behavior on desktop viewports: - hide + [sm] = hide only at 640-767. At 1200px → visible. - show + [sm] = show only at 640-767. At 1200px → hidden. Both are mathematically correct, but the direction toggle mixed with per-range checkboxes has no intuitive mental model — the editor reads it as "hide direction always shows, show direction always hides." The WordPress block-visibility plugin the issue references solves this by not having a direction toggle at all: check the screen sizes to hide on. Match that. The UI now reads "Hide on: [bp checkboxes]". The rule + PHP-side schema still honor `direction: 'show'` for backwards compat with any content that already persisted it — the UI just pins new writes to `direction: 'hide'`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The hardcoded fallback in `with-visibility-panel.tsx` only rendered
three breakpoints (sm / md / lg). Users on desktop viewports ≥1280px
(xl / 2xl range) had no way to hide blocks in their own viewport:
checking all three visible options still left the block visible on
their screen.
Wire the panel to the same `viewportRegistry` the site-editor's
viewport switcher already hydrates from the server-stamped
`data-breakpoints` snapshot. Publishes `[{key, label}]` via a
`setVisibilityBreakpoints()` setter on the visibility module so the
HOC picks up host-overridden labels alongside the default
Tailwind-v4 five-tier set.
Also bumps the fallback (used when the editor never calls the setter
— e.g. inside a lone Vitest render) to cover all five defaults with
Tailwind-style labels so users see a full option set even if the
snapshot never lands.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… correctness)
Applies fixes for every finding surfaced by the /code-review high-effort
pass. Grouped here for one atomic commit; each change is scoped to its
own file(s) for review.
## Security
1. UsersSearchController gates on `SiteEditorAccessGate::check()` (the
bound editor-permission gate every other admin route uses) instead
of `Auth::check()`. Previously any authenticated visitor —
including customer-facing memberships, cms-framework subscribers,
e-commerce accounts — could enumerate every user in the DB via
`?q=<letter>` iteration. Now returns 403 for non-editor sessions.
2. LIKE metacharacters (`%` / `_` / `|`) escaped via the same pattern
`EntitySearchController::buildLikeQuery()` uses (`ESCAPE '|'`).
Blocks the `?q=%25` amplification path that would otherwise
collapse the pattern to `%%%` and dump 20 users per request.
9. `TreePruner::walk()` and `BlockRenderer::renderInner()` gain a
`MAX_INNER_DEPTH = 128` recursion cap with `report()` on overflow.
Prevents a persisted deeply-nested block payload (compromised
import, editor bug, malicious pattern PUT) from stack-overflowing
the PHP process on every subsequent visit.
## Renderer parity
3. React + Vue `BlockTree` now pipe the incoming tree through
`filterVisibleBlocks()` + `stampVisibilityScopes()`, emit the
accumulated CSS as a single `<style data-ve-visibility>` block, and
wrap each scoped block in a `display:contents` wrapper carrying
the `_veVisScope` class. Previously the server pruner stamped
`_veHiddenBreakpoints` but nothing on the client consumed it — the
screen-size rule was silently non-functional on React/Vue targets.
4. `editor-app.tsx` publishes host-supplied `visibilityRoles` prop via
`setVisibilityRoles()` in a new useEffect. Adds the prop to
`EditorAppProps`. Without this, the setter was defined but never
called and the User Role subsection's chip list was always empty.
## Correctness
5. `RecurringScheduleRule::matchWindow()` now handles overnight
windows (e.g. Sat 22:00 → Sun 02:00) by matching on EITHER the
start day (from `start` onwards) OR the following day-of-week (up
to `end`). Previously `end < start` was treated as invalid and
the window silently never fired.
6. UUID / non-numeric user primary keys survive round-trip through
`UsersSearchController::query()` and `SpecificUserRule` — the
`is_numeric` guard is replaced with a scalar/non-empty check, ids
are preserved as `int|string` end-to-end (also widens
`VisibilityContext::userId`, `PreviewContext::userId`, and the TS
`SpecificUserRef.id`). Fixes the Specific User picker on any host
using `HasUuids`.
7. `ReferrerRule::matchPattern()` wildcard branch lowercases the
suffix so brand-cased patterns like `*.Example.com` match
`foo.example.com`. The literal branch was already
case-insensitive via `strcasecmp`; this brings the wildcard
branch to parity.
8. Documented the identifier-shape contract on
`VisibilityEvaluator::resolveRoles()` — both this method and the
picker's `setVisibilityRoles` must agree on the identifier shape
(slug on cms-framework hosts, name on Spatie, whatever the
host's `->roles` relation exposes). Silent mismatch on install
swap was the biggest failure mode.
10. `applyFilters('ap.visual-editor.visibility.register-rules')`
moved from inside the scoped closure to a boot-time
`container->extend()` layer. Guarantees packages that register
their custom rules via `addFilter` in their own `boot()` are
always visible, even if an intra-boot resolve captured the
registry before their boot ran.
## Tests
- 4 new Pest tests: overnight window, UUID user rule, referrer
wildcard case, LIKE metacharacter escape.
- 1 replaced Pest test: the `401 for anon` case is now
`403 for non-editor via gate` (the endpoint no longer accepts any
authenticated session).
- Vitest suite unchanged (client-side helpers are backwards compat).
Test totals: 1474 Pest pass (was 1470, +4 new coverage), 2422 Vitest
pass. Only the pre-existing 17 `Tests\Feature\Ai\*` env failures
remain.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…block
Adds cms-framework Dynamic Content integration to the visual-editor:
new `dynamic_content` block-binding source, batched resolve + sources
REST endpoints, `{{` autocomplete + token chip decoration + Token
Inserter modal, link picker Dynamic Content tab, Image block DC
binding UI, reusable `artisanpack/snippet` block with cycle-guarded
CRUD + admin, and `artisanpack/dynamic-loop` block that iterates a
collection source per record. SSR wires binding resolution into the
Blade renderer via an optional `BindingResolver` dependency so trees
without bindings round-trip byte-identically. Adds `WantsInnerBlocks`
marker so dynamic blocks that need the inner tree at render time
(loop) receive it. cms-framework is a soft dep — everything degrades
cleanly when absent. Pest + Vitest coverage for the source resolver,
endpoints, snippet CRUD + cycle guard, loop SSR, autocomplete API
client, and token inserter modal. Docs: new dynamic-content.md,
reconciled block-bindings.md staleness (bindings sidecar, `fallback`
policy, corrected endpoint paths).
Closes #650
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… button binding Follow-up on #650 visual QA: - SSR: resolveInlineTokens pass in BlockRenderer walks every string attr and runs `{{token}}` occurrences through cms-framework's DynamicContentResolver, so authored inline tokens (paragraph content, heading content, etc.) render as their resolved value on the front end. Previously only `bindings` sidecars were resolved, leaving inline `{{team.role}}` unresolved. - Toolbar: replace the dashicon string `editor-code` (unresolved in block-editor v15+) with an inline SVG token glyph so the Insert Dynamic Content toolbar button actually renders. - CSS: ship dynamic-content.css covering the Token Inserter modal (grid rows + type badges + preview strip), chip decoration inside RichText, snippet + dynamic-loop placeholders, and link-picker DC tab. Previously the modal referenced class hooks with no styles. - Button binding panel: mirror the Image binding pattern to bind the Button block's `url` attribute to a DC token — the inline __experimentalLinkControl popover has no extension seam. Phone / email fields automatically pass `args.scheme` (`tel|mailto`) so the source prefixes the resolved value; guarded so already-schemed values don't double-prefix. Adds 3 Pest tests for the new args.scheme behavior. Suite green (29 source assertions across 13 tests). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lain-SVG toolbar icon - Button/Image binding panel HOCs now gate their InspectorControls fill on `useSelect(getSelectedBlockClientId) === props.clientId`. Without the gate, container blocks that mount inner children's edit components (e.g. `artisanpack/buttons` around `artisanpack/button`) stack duplicate "Dynamic Content" panels in the sidebar. - Toolbar HOC gets the same selection gate for its BlockControls + Modal fill (only the actively-selected block should own the toolbar button + modal instance). - Runtime style injection replaces the CSS import chain. Vite's transitive CSS discovery through the deep editor bundle is host- build-dependent — some hosts drop the stylesheet from the graph. Injecting once at boot via a data-marker'd <style> tag guarantees the Token Inserter modal grid, chip decoration, and snippet placeholder styles apply everywhere. The `.css` file stays for hosts that want to import it directly. - Toolbar icon switches to a plain React <svg> (was @wordpress/components SVG+Path) so ToolbarButton renders the glyph consistently across block-editor versions. - Drop BlockControls `group="other"` in favor of the default group so the button always lands in the visible toolbar row. - New Pest suite RendererInlineTokensTest covers the inline-token pass: resolves tokens in string attrs, leaves clean attrs alone, recurses into innerBlocks, ignores non-string attrs. Suite green: 33 tests / 68 assertions (was 26 / 60). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…r token preview
- Token Inserter modal now uses inline React styles instead of a
className+stylesheet pair. Removes the last dependency on the
host's Vite CSS traversal, which was silently dropping the
dynamic-content stylesheet in some builds.
- Drop the `useSelect` gate around the toolbar's BlockControls —
BlockControls is already auto-scoped to the selected block by
Gutenberg's block list, and the gate caused the icon to
intermittently vanish during selection transitions.
- New editor live preview module installs a MutationObserver that
scans the editor canvas for `{{token}}` text and swaps in the
resolved value (or a `[Missing: token]` marker) as an inline
span. Users see resolved values in the editor immediately without
waiting for the server render. Save-time serialization is
unaffected — the raw `{{token}}` text is what persists; the
wrapper spans only exist in the live DOM.
- The observer skips `code`, `pre`, `.block-editor-plain-text`, and
`.cm-editor` so tokens inside literal-content blocks are not
substituted. Loop-safe: mutations inside our own wrapper spans
short-circuit the observer to prevent feedback loops.
Suite still green (33 tests / 68 assertions).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two block shapes coexist in this codebase:
- Gutenberg's editor serializes to `{name, attributes, innerBlocks}`
with `bindings` nested inside `attributes`. This is what lands in
the DB via the React editor's save path.
- WordPress's `parse_blocks()` produces `{name, attrs, innerBlocks}`
with `bindings` as a top-level sidecar. This is what post-content
HTML-comment parsing yields.
Previously my `resolveInlineTokens` and the existing `BindingResolver`
only read `attrs` + top-level `bindings`. Blocks persisted by the
editor stayed unresolved — that's why `{{team.role}}` rendered
literally on the front end and Button bindings didn't fold into
`<a href>` at render.
Both resolvers now check `attributes` first, `attrs` as fallback,
and write resolved values back to whichever key the block used
(preserving round-trip fidelity). Bindings are read from top-level
first, then from `attributes.bindings` — covering both Gutenberg's
and parse_blocks's placement.
Adds a test locking in the Gutenberg-shape resolution path.
Suite: 110 tests / 214 assertions, all green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ctness, Octane Applies all 10 findings from the high-effort code-review pass: Security: - Snippet CRUD (index/store/show/update/destroy) now gates on SiteEditorAccessGate, mirroring the icon-set management pattern. SnippetControllerTest covers both allow and deny paths. - DynamicContentResolveController gates on SiteEditorAccessGate and the route carries `throttle:60,1` — no more unauthenticated batch resolution or DoS surface. - DynamicContentSource::applyScheme enforces a URL scheme allowlist aligned with UrlSanitizer::SAFE_SCHEMES. Values whose existing scheme is unsafe (javascript:/data:/vbscript:/file:) resolve to null so the empty-value policy kicks in — no more raw javascript: URLs sneaking into bound `href` attributes. Test covers each vector. Correctness bugs from the review: - New Support\BlockShape helper centralizes attributes-vs-attrs detection and provides normalizeTree(). BlockRenderer::render() normalizes once at entry; downstream stages (SnippetCycleGuard, BindingResolver, resolveInlineTokens) go through BlockShape so a future pipeline stage can't silently miss the editor shape. - SnippetCycleGuard was reading only `attrs` — editor-persisted snippets bypassed cycle detection at save. Now reads via BlockShape. - BindingResolver's `attrs.bindings` sidecar read now requires at least one entry with a `source` key, preventing a hypothetical block with an unrelated `bindings` attribute from being misread. - BlockPreviewController now checks WantsInnerBlocks and forwards innerBlocks to renderWithInner(). This is the endpoint the React and Vue renderers hit for server HTML, so DynamicLoopBlock now renders correctly on all three surfaces, not Blade-only. Octane safety: - SnippetBlock stops storing $visited/$depth as instance state on the container-shared singleton. Cycle scope now lives in a request-scoped container binding (`ve.snippet.visited`) that Laravel flushes between requests under Octane/Swoole/RoadRunner. Cache correctness: - api.ts resolveTokens no longer caches null (missing) values. Previously a token requested before its DC type was registered would show `[Missing:…]` forever in a session because the null hit short-circuited every subsequent request. Cleanup: - Delete dead code: link-control.tsx (not wired — tracked in #662), dynamic-content.css (was duplicated by inject-styles.ts and imported nowhere), token-chip-format.ts (registerFormatType fired but decorateTokens was never applied). Corresponding CSS blocks removed from inject-styles.ts. Test suite: 86 Pest / 4099 assertions passing (+5 new tests for the scheme allowlist, editor-shape cycle detection, and gate denial). 7 Vitest / all passing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rk classes aren't autoloaded CI installs `artisanpack-ui/cms-framework: ^2.0`, but Dynamic Content (DynamicContentResolver, DynamicContentAccessor, DynamicContentTypeRegistry) first shipped in 2.4. When CI resolves a 2.0.x tag, `class_exists()` against those FQCNs returns false and the guards short-circuit — even though the test fake is bound via `app()->instance($fqcn, $fake)` and would resolve correctly. Widen every cms-framework class-existence guard to also accept a container-bound fake: if (! class_exists($fqcn) && ! app()->bound($fqcn)) return early; Also move `BlockShape::normalizeTree()` above the resolver guard in `BlockRenderer::resolveInlineTokens()` so the canonical shape is returned even when the resolver isn't available. Tests calling the public helper directly (RendererInlineTokensTest) rely on that guarantee. No behavior change for production hosts that install cms-framework ≥2.4 — both branches of the `||` are true, guard passes normally. For hosts on 2.0–2.3, the DC surface stays a no-op exactly as before. Suite: 41 DC/Snippet/Renderer tests passing locally; the CI failure mode (17 RendererInlineTokensTest failures on missing `attributes` key) should now clear. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nt-editor-ux feat(dynamic-content): editor UX, block bindings, snippets, and loop block (#650)
- Bump version to 1.4.0 in composer.json and package.json - Convert [Unreleased] to [1.4.0] - 2026-07-16 in CHANGELOG - Add missing 1.4.0 changelog entries: Dynamic Content editor UX (#650), Block Visibility (#491, #492, #493), responsive preview device sizes (#617), and the three new extension filters (background-controls, rendered-block, canvas-styles) - Link Visibility and Dynamic Content docs from docs/home.md; bump doc footer version - Sync composer.lock content-hash (regenerated with dev-only path repositories stripped, matching the CI resolution) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 267 files, which is 167 over the limit of 100. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (267)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Release Information
release/1.4mainRelease Summary
v1.4.0 adds three large author-facing surfaces (Block Visibility,
Dynamic Content editor UX, and CSS Positioning), a Gutenberg-style
page pattern inserter modal for starter layouts, and a unified
device-preview control on the top-bar viewport switcher. It also
opens three new extension filters (
ap.visual-editor.background-controls,ap.visual-editor.rendered-block,ap.visual-editor.canvas-styles)that let downstream packages contribute to the appearance panel,
post-process rendered block HTML, and inject stylesheets into the
Gutenberg canvas iframe without patching the visual-editor package.
Changelog
Added
integration inside the visual editor:
dynamic_contentblock-bindingsource, batched resolve + sources REST endpoints (gated by
SiteEditorAccessGate, throttled 60/1),{{autocomplete + tokenchip decoration + Token Inserter modal, a Dynamic Content tab on
the link picker, and an Image block DC binding UI. Ships two new
blocks:
artisanpack/snippet(cycle-guarded CRUD + admin surface)and
artisanpack/dynamic-loop(iterates a collection source perrecord). cms-framework is a soft dep — everything degrades cleanly
when absent. New
docs/dynamic-content.md.visibility rules that evaluate server-side so hidden blocks
never emit markup. Three rule families: contextual (screen size,
query string, referrer, browser/OS/device), user & auth (login
state, role, specific user), and scheduling (single date/time
window, recurring weekly schedule, per-rule timezone override,
overnight window support). All three renderers (Blade, React, Vue)
emit
<style data-ve-visibility>media queries for the screen-sizerule. New
docs/visibility.md.a unified device-preview + edit-scope control — selecting a preset
atomically resizes the canvas iframe and scopes subsequent style
edits.
Breakpointgainslabel+previewWidthPx; PHP configaccepts either the legacy scalar shape or the new object shape.
pattern" modal that auto-opens on a never-saved record with no
content, and can be re-opened from a top-bar button. Optional
template selector for the
pagesresource.PatternControlleraccepts
?post_type={slug}for server-side scope filtering; everycontributor to the
ap.visual-editor.patternsfilter may now carryan optional
post_typesscope.Per-block Position panel with
static/relative/absolute/fixed/stickydropdown, per-side offsets (
px/%/rem/em/vh/vw/auto), z-index, andper-breakpoint overrides. Opt-in via
supports.position: true;enabled on 82 top-level artisanpack blocks. Emits scoped CSS in the
editor + inline styles + a
<style data-ve-position>block on thefrontend via the Blade renderer. New
docs/position.md.ap.visual-editor.background-controlsfilter (Addap.visual-editor.background-controlsfilter for external packages to extend the background/appearance panel #649). New JSfilter that lets third-party packages contribute panels to the
background/appearance area of any block that opts into a background
support — no per-package
editor.BlockEditHOC required.ap.visual-editor.rendered-blockfilter (PHP). Fires inBlockRenderer::renderBlockafter each block finishes rendering.Lets cross-cutting effects (frosted glass, contrast overlays, motion
wrappers) post-process markup without editing the renderer.
ap.visual-editor.canvas-stylesfilter (JS). Filters theordered
CanvasStyle[]handed toBlockCanvas's__unstableEditorStylesprop so packages can push theirstylesheets into the sandboxed Gutenberg iframe.
Upgrade notes
php artisan vendor:publish --tag=visual-editor-blade-viewson a prior version must re-publishwith
--force— the publishedblocks.blade.phpandtemplate.blade.phpfiles shadow the package source, and thepre-1.4 copies don't include the new
<style data-ve-position>output block, so sticky/absolute blocks render unpositioned on the
frontend.
Issues and Pull Requests
Issues Fixed:
ap.visual-editor.background-controlsfilter for external packages to extend the background/appearance panel #649Related PRs:
Breaking Changes
Testing Checklist
failures (documented in prior release — the AI package requires
PHP 8.3+ and the dev machine's vendor tree does not currently
contain it; CI on 8.3/8.4 passes the AI suite).
composer audit,npm audit)Documentation Checklist
Snippets, and Users Search
but the "Upgrade notes" block above covers the mandatory
vendor:publish --forcestepPre-Release Checklist
(
composer.json,package.json,docs/home.mdfooter)v1.4.0(to be created on merge)release/1.4synced against the CI-shape composer.json)
Dependency Notes
composer auditsurfaces 20 advisories against 11 packages, all inthe transitive dev/test dependency tree (guzzle, phpunit,
laravel/framework testbench deps, symfony/{http-foundation,
http-kernel, mailer, mime, polyfill-intl-idn, routing, yaml}). None
touch the shipped runtime code and none are actionable without a
testbench/orchestra bump. Flag for a follow-up dev-dep sweep.
npm audit --productionsurfaces 18 advisories (17 moderate, 1high). The high is
linkify-itreached transitively through the@wordpress/*block-editor stack, gated on npm's suggested versionbumps of upstream WordPress packages. Not blocking this release.
Notes
The [Unreleased] section of
CHANGELOG.mdcame into this releasecontaining only the Position + Page Pattern Modal entries. The
release-prep commit expands it to cover every 1.4-scoped feature
that landed on the branch (Dynamic Content, Block Visibility,
Responsive Preview, and the three new extension filters) so the
public changelog matches what shipped.
composer.lockwas regenerated with the dev-only path repositoriesstripped — the resulting artifact matches what CI produces on merge
(the
_comments.repositoriesnote incomposer.jsoncalls this out).The only intentional payload of this regeneration is the
content-hashbump for the version field change; the churn onartisanpack-ui/{ai,cms-framework,icons}entries just re-canonicalizesthose blocks from local path refs back to their upstream git refs.
Release Manager: @jacob-martella
For Reviewers:
branch — especially the newly-added Dynamic Content, Block
Visibility, and extension-filter entries
composer.json,package.json,docs/home.md)