Skip to content

Release v1.4.0 - #663

Merged
ViewFromTheBox merged 34 commits into
mainfrom
release/1.4
Jul 17, 2026
Merged

ViewFromTheBox merged 34 commits into
mainfrom
release/1.4

Conversation

@ViewFromTheBox

Copy link
Copy Markdown
Member

Release Information

  • Release Version: v1.4.0
  • Release Date: 2026-07-16
  • Release Type: Minor
  • Release Branch: release/1.4
  • Target Branch: main

Release 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

Upgrade notes

  • Hosts that ran php artisan vendor:publish --tag=visual-editor-blade-views on a prior version must re-publish
    with --force
    — the published blocks.blade.php and
    template.blade.php files shadow the package source, and the
    pre-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:

Related PRs:

Breaking Changes

  • No breaking changes
  • Breaking changes documented below

Testing Checklist

  • All automated tests passing
    • Pest: 1515 passing, 1 skipped, 17 pre-existing AI env
      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).
    • Vitest: 2429 passing across 280 test files.
  • Security scan completed (composer audit, npm audit)
  • Manual testing completed
  • Accessibility tests passing
  • Performance tests passing (if applicable)
  • Tested in staging environment
  • Database migrations tested (if applicable)

Documentation Checklist

  • CHANGELOG updated
  • README updated (if needed) — no changes required
  • API documentation updated — new endpoints for Dynamic Content,
    Snippets, and Users Search
  • Migration guide written — not required (no breaking changes),
    but the "Upgrade notes" block above covers the mandatory
    vendor:publish --force step
  • Release notes written (this PR)
  • Wiki updated (if applicable)

Pre-Release Checklist

  • Version number updated in all necessary files
    (composer.json, package.json, docs/home.md footer)
  • Git tag prepared: v1.4.0 (to be created on merge)
  • Release branch created/updated: release/1.4
  • Dependencies updated and tested (composer.lock content-hash
    synced against the CI-shape composer.json)
  • Build artifacts generated
  • Deployment plan reviewed
  • Rollback plan prepared
  • Stakeholders notified

Dependency Notes

composer audit surfaces 20 advisories against 11 packages, all in
the 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 --production surfaces 18 advisories (17 moderate, 1
high). The high is linkify-it reached transitively through the
@wordpress/* block-editor stack, gated on npm's suggested version
bumps of upstream WordPress packages. Not blocking this release.

Notes

The [Unreleased] section of CHANGELOG.md came into this release
containing 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.lock was regenerated with the dev-only path repositories
stripped — the resulting artifact matches what CI produces on merge
(the _comments.repositories note in composer.json calls this out).
The only intentional payload of this regeneration is the
content-hash bump for the version field change; the churn on
artisanpack-ui/{ai,cms-framework,icons} entries just re-canonicalizes
those blocks from local path refs back to their upstream git refs.


Release Manager: @jacob-martella

For Reviewers:

  • Verify all checklist items completed
  • Review changelog accuracy against the feature commits in this
    branch — especially the newly-added Dynamic Content, Block
    Visibility, and extension-filter entries
  • Confirm version numbers correct (1.4.0 across composer.json,
    package.json, docs/home.md)
  • Check that no breaking changes slipped in
  • Approve deployment plan

ViewFromTheBox and others added 30 commits July 11, 2026 13:29
…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>
…orts-tests-docs

feat: position support opt-in, tests, docs (#645, #646, #647, #648)
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>
feat: Block Visibility — contextual, user/auth, scheduling rules (#491 · #492 · #493)
…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>
ViewFromTheBox and others added 4 commits July 16, 2026 19:46
…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>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 267 files, which is 167 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 31915e0b-c6fb-428b-87f2-8dea0566ab4f

📥 Commits

Reviewing files that changed from the base of the PR and between 255a11d and f9c0463.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock, !composer.lock
📒 Files selected for processing (267)
  • CHANGELOG.md
  • composer.json
  • config/visual-editor.php
  • database/factories/SnippetFactory.php
  • database/migrations/2026_07_16_000000_create_ve_snippets_table.php
  • docs/Hooks-and-Events.md
  • docs/block-bindings.md
  • docs/blocks/Responsive-Design-Tools.md
  • docs/dynamic-content.md
  • docs/home.md
  • docs/plans/21-block-visibility-contextual.md
  • docs/plans/22-block-visibility-user-and-auth.md
  • docs/plans/23-block-visibility-scheduling.md
  • docs/plans/font-library.md
  • docs/position.md
  • docs/visibility.md
  • package.json
  • packages/visual-editor-renderer-blade/resources/views/components/blocks.blade.php
  • packages/visual-editor-renderer-blade/resources/views/components/template.blade.php
  • packages/visual-editor-renderer-blade/src/BlockRenderer.php
  • packages/visual-editor-renderer-blade/src/Services/PositionCssAccumulator.php
  • packages/visual-editor-renderer-blade/src/Support/BlockSupports.php
  • packages/visual-editor-renderer-blade/src/View/Components/BlocksComponent.php
  • packages/visual-editor-renderer-blade/src/View/Components/TemplateComponent.php
  • packages/visual-editor-renderer-blade/src/VisualEditorRendererBladeServiceProvider.php
  • packages/visual-editor-renderer-blade/tests/Feature/BlockRendererVisibilityTest.php
  • packages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.php
  • packages/visual-editor-renderer-blade/tests/Unit/BlockSupportsTest.php
  • packages/visual-editor-renderer-blade/tests/Unit/PositionCssAccumulatorTest.php
  • packages/visual-editor-renderer-react/src/BlockTree.tsx
  • packages/visual-editor-renderer-react/src/index.ts
  • packages/visual-editor-renderer-react/src/visibility.ts
  • packages/visual-editor-renderer-react/tests/visibility.test.ts
  • packages/visual-editor-renderer-vue/src/BlockTree.ts
  • packages/visual-editor-renderer-vue/src/index.ts
  • packages/visual-editor-renderer-vue/src/visibility.ts
  • resources/js/visual-editor/background-controls/__tests__/background-controls.test.ts
  • resources/js/visual-editor/background-controls/__tests__/with-background-controls.test.tsx
  • resources/js/visual-editor/background-controls/background-controls.ts
  • resources/js/visual-editor/background-controls/index.ts
  • resources/js/visual-editor/background-controls/with-background-controls.tsx
  • resources/js/visual-editor/blocks/accordions/block.json
  • resources/js/visual-editor/blocks/archives/block.json
  • resources/js/visual-editor/blocks/audio/block.json
  • resources/js/visual-editor/blocks/author-social-icons/block.json
  • resources/js/visual-editor/blocks/avatar/block.json
  • resources/js/visual-editor/blocks/breadcrumbs/block.json
  • resources/js/visual-editor/blocks/buttons/block.json
  • resources/js/visual-editor/blocks/callout/block.json
  • resources/js/visual-editor/blocks/categories/block.json
  • resources/js/visual-editor/blocks/code/block.json
  • resources/js/visual-editor/blocks/columns/block.json
  • resources/js/visual-editor/blocks/comment-author-avatar/block.json
  • resources/js/visual-editor/blocks/comment-author-name/block.json
  • resources/js/visual-editor/blocks/comment-content/block.json
  • resources/js/visual-editor/blocks/comment-date/block.json
  • resources/js/visual-editor/blocks/comment-edit-link/block.json
  • resources/js/visual-editor/blocks/comment-reply-link/block.json
  • resources/js/visual-editor/blocks/comments-number/block.json
  • resources/js/visual-editor/blocks/comments-pagination/block.json
  • resources/js/visual-editor/blocks/comments/block.json
  • resources/js/visual-editor/blocks/copyright/block.json
  • resources/js/visual-editor/blocks/cover/block.json
  • resources/js/visual-editor/blocks/details/block.json
  • resources/js/visual-editor/blocks/dynamic-loop/block.json
  • resources/js/visual-editor/blocks/dynamic-loop/edit.tsx
  • resources/js/visual-editor/blocks/dynamic-loop/index.ts
  • resources/js/visual-editor/blocks/dynamic-loop/save.tsx
  • resources/js/visual-editor/blocks/embed/block.json
  • resources/js/visual-editor/blocks/file/block.json
  • resources/js/visual-editor/blocks/form/block.json
  • resources/js/visual-editor/blocks/gallery/block.json
  • resources/js/visual-editor/blocks/grid/block.json
  • resources/js/visual-editor/blocks/heading/block.json
  • resources/js/visual-editor/blocks/icon/block.json
  • resources/js/visual-editor/blocks/image/block.json
  • resources/js/visual-editor/blocks/latest-posts/block.json
  • resources/js/visual-editor/blocks/list/block.json
  • resources/js/visual-editor/blocks/loginout/block.json
  • resources/js/visual-editor/blocks/marquee/block.json
  • resources/js/visual-editor/blocks/media-text/block.json
  • resources/js/visual-editor/blocks/navigation/block.json
  • resources/js/visual-editor/blocks/next-post/block.json
  • resources/js/visual-editor/blocks/paragraph/block.json
  • resources/js/visual-editor/blocks/post-author-biography/block.json
  • resources/js/visual-editor/blocks/post-author-name/block.json
  • resources/js/visual-editor/blocks/post-author/block.json
  • resources/js/visual-editor/blocks/post-comments-count/block.json
  • resources/js/visual-editor/blocks/post-comments-form/block.json
  • resources/js/visual-editor/blocks/post-comments-link/block.json
  • resources/js/visual-editor/blocks/post-comments-title/block.json
  • resources/js/visual-editor/blocks/post-content/block.json
  • resources/js/visual-editor/blocks/post-date/block.json
  • resources/js/visual-editor/blocks/post-excerpt/block.json
  • resources/js/visual-editor/blocks/post-featured-image/block.json
  • resources/js/visual-editor/blocks/post-navigation-link/block.json
  • resources/js/visual-editor/blocks/post-terms/block.json
  • resources/js/visual-editor/blocks/post-title/block.json
  • resources/js/visual-editor/blocks/post-types-search-results/block.json
  • resources/js/visual-editor/blocks/preformatted/block.json
  • resources/js/visual-editor/blocks/previous-post/block.json
  • resources/js/visual-editor/blocks/pullquote/block.json
  • resources/js/visual-editor/blocks/query-title/block.json
  • resources/js/visual-editor/blocks/query/block.json
  • resources/js/visual-editor/blocks/quote/block.json
  • resources/js/visual-editor/blocks/read-more/block.json
  • resources/js/visual-editor/blocks/related-posts/block.json
  • resources/js/visual-editor/blocks/search-field/block.json
  • resources/js/visual-editor/blocks/search-filters-buttons/block.json
  • resources/js/visual-editor/blocks/search-filters-taxonomy/block.json
  • resources/js/visual-editor/blocks/search-filters/block.json
  • resources/js/visual-editor/blocks/search/block.json
  • resources/js/visual-editor/blocks/separator/block.json
  • resources/js/visual-editor/blocks/single-content/block.json
  • resources/js/visual-editor/blocks/site-logo/block.json
  • resources/js/visual-editor/blocks/site-tagline/block.json
  • resources/js/visual-editor/blocks/site-title/block.json
  • resources/js/visual-editor/blocks/skills-slider/block.json
  • resources/js/visual-editor/blocks/snippet/block.json
  • resources/js/visual-editor/blocks/snippet/edit.tsx
  • resources/js/visual-editor/blocks/snippet/index.ts
  • resources/js/visual-editor/blocks/snippet/save.tsx
  • resources/js/visual-editor/blocks/social-share-content/block.json
  • resources/js/visual-editor/blocks/spacer/block.json
  • resources/js/visual-editor/blocks/table/block.json
  • resources/js/visual-editor/blocks/tabs/block.json
  • resources/js/visual-editor/blocks/tag-cloud/block.json
  • resources/js/visual-editor/blocks/template-part/block.json
  • resources/js/visual-editor/blocks/term-description/block.json
  • resources/js/visual-editor/blocks/verse/block.json
  • resources/js/visual-editor/blocks/video/block.json
  • resources/js/visual-editor/dynamic-content/__tests__/api.test.ts
  • resources/js/visual-editor/dynamic-content/__tests__/token-inserter-modal.test.tsx
  • resources/js/visual-editor/dynamic-content/api.ts
  • resources/js/visual-editor/dynamic-content/autocomplete.ts
  • resources/js/visual-editor/dynamic-content/button-binding-panel.tsx
  • resources/js/visual-editor/dynamic-content/editor-live-preview.ts
  • resources/js/visual-editor/dynamic-content/image-binding-panel.tsx
  • resources/js/visual-editor/dynamic-content/index.ts
  • resources/js/visual-editor/dynamic-content/inject-styles.ts
  • resources/js/visual-editor/dynamic-content/token-inserter-modal.tsx
  • resources/js/visual-editor/dynamic-content/toolbar-button.tsx
  • resources/js/visual-editor/editor/__tests__/canvas-styles.filter.test.ts
  • resources/js/visual-editor/editor/__tests__/editor-canvas.test.tsx
  • resources/js/visual-editor/editor/__tests__/top-bar.test.tsx
  • resources/js/visual-editor/editor/canvas-styles.ts
  • resources/js/visual-editor/editor/editor-app.css
  • resources/js/visual-editor/editor/editor-app.tsx
  • resources/js/visual-editor/editor/editor-canvas.tsx
  • resources/js/visual-editor/editor/main.tsx
  • resources/js/visual-editor/editor/page-pattern-modal/__tests__/filter-modal-patterns.test.ts
  • resources/js/visual-editor/editor/page-pattern-modal/__tests__/page-pattern-modal.test.tsx
  • resources/js/visual-editor/editor/page-pattern-modal/__tests__/use-fresh-content-detection.test.ts
  • resources/js/visual-editor/editor/page-pattern-modal/filter-modal-patterns.ts
  • resources/js/visual-editor/editor/page-pattern-modal/page-pattern-modal.css
  • resources/js/visual-editor/editor/page-pattern-modal/page-pattern-modal.tsx
  • resources/js/visual-editor/editor/page-pattern-modal/use-fresh-content-detection.ts
  • resources/js/visual-editor/editor/top-bar.tsx
  • resources/js/visual-editor/positioning/__tests__/emitter.test.ts
  • resources/js/visual-editor/positioning/__tests__/extend-supports.test.ts
  • resources/js/visual-editor/positioning/__tests__/integration.test.ts
  • resources/js/visual-editor/positioning/__tests__/resolver.test.ts
  • resources/js/visual-editor/positioning/emitter.ts
  • resources/js/visual-editor/positioning/extend-supports.ts
  • resources/js/visual-editor/positioning/index.ts
  • resources/js/visual-editor/positioning/register.ts
  • resources/js/visual-editor/positioning/resolver.ts
  • resources/js/visual-editor/positioning/types.ts
  • resources/js/visual-editor/positioning/with-position-control.tsx
  • resources/js/visual-editor/positioning/with-position-styles.tsx
  • resources/js/visual-editor/responsive/InspectorScopeChip.tsx
  • resources/js/visual-editor/responsive/ViewportSwitcher.tsx
  • resources/js/visual-editor/responsive/__tests__/ViewportSwitcher.test.tsx
  • resources/js/visual-editor/responsive/__tests__/registry.test.ts
  • resources/js/visual-editor/responsive/registry.ts
  • resources/js/visual-editor/responsive/types.ts
  • resources/js/visual-editor/responsive/use-canvas-preview-width.ts
  • resources/js/visual-editor/site-editor/__tests__/site-editor-app.test.tsx
  • resources/js/visual-editor/site-editor/canvas-frame.css
  • resources/js/visual-editor/site-editor/canvas-frame.tsx
  • resources/js/visual-editor/site-editor/main.tsx
  • resources/js/visual-editor/site-editor/patterns/api-client.ts
  • resources/js/visual-editor/site-editor/site-editor-app.tsx
  • resources/js/visual-editor/visibility/VisibilityPanel.tsx
  • resources/js/visual-editor/visibility/register-attribute.ts
  • resources/js/visual-editor/visibility/types.ts
  • resources/js/visual-editor/visibility/user-search.ts
  • resources/js/visual-editor/visibility/with-visibility-panel.tsx
  • resources/views/admin/snippets.blade.php
  • resources/views/components/editor.blade.php
  • resources/views/editor/mount.blade.php
  • resources/views/site-editor/index.blade.php
  • routes/api.php
  • routes/web.php
  • src/Blocks/DynamicContent/DynamicLoopBlock.php
  • src/Blocks/DynamicContent/SnippetBlock.php
  • src/Blocks/WantsInnerBlocks.php
  • src/Console/AuditScheduledBlocksCommand.php
  • src/Http/Controllers/BlockPreviewController.php
  • src/Http/Controllers/DynamicContent/DynamicContentResolveController.php
  • src/Http/Controllers/DynamicContent/DynamicContentSourcesController.php
  • src/Http/Controllers/DynamicContent/SnippetController.php
  • src/Http/Controllers/Requests/SnippetRequest.php
  • src/Http/Controllers/SiteEditor/PatternController.php
  • src/Http/Controllers/Visibility/UsersSearchController.php
  • src/Http/Requests/BlockPreviewRequest.php
  • src/Http/Resources/Adapters/CmsFramework/SiteEditor/PatternAdapter.php
  • src/Models/Snippet.php
  • src/Position/PositionEmitter.php
  • src/Position/PositionResolver.php
  • src/Responsive/BreakpointRegistry.php
  • src/Services/Bindings/BindingResolver.php
  • src/Services/Bindings/Sources/DynamicContentSource.php
  • src/Services/DynamicContent/SnippetCycleException.php
  • src/Services/DynamicContent/SnippetCycleGuard.php
  • src/SiteEditor/Resolution/ResolvedPattern.php
  • src/Support/BlockShape.php
  • src/View/Components/VisualEditorComponent.php
  • src/Visibility/PreviewContext.php
  • src/Visibility/RuleRegistry.php
  • src/Visibility/Rules/BrowserOsDeviceRule.php
  • src/Visibility/Rules/DateTimeWindowRule.php
  • src/Visibility/Rules/HideRule.php
  • src/Visibility/Rules/LoginStateRule.php
  • src/Visibility/Rules/QueryStringRule.php
  • src/Visibility/Rules/RecurringScheduleRule.php
  • src/Visibility/Rules/ReferrerRule.php
  • src/Visibility/Rules/ScreenSizeRule.php
  • src/Visibility/Rules/SpecificUserRule.php
  • src/Visibility/Rules/UserRoleRule.php
  • src/Visibility/ScheduledBlockCollector.php
  • src/Visibility/TreePruner.php
  • src/Visibility/UserAgentParser.php
  • src/Visibility/VisibilityContext.php
  • src/Visibility/VisibilityDecision.php
  • src/Visibility/VisibilityEvaluator.php
  • src/Visibility/VisibilityRule.php
  • src/VisualEditorServiceProvider.php
  • tests/E2E/positioning.spec.ts
  • tests/E2E/visibility.spec.ts
  • tests/Feature/SiteEditor/PatternControllerStandaloneTest.php
  • tests/Feature/SiteEditor/PatternControllerTest.php
  • tests/Feature/VisualEditor/DynamicContentResolveControllerTest.php
  • tests/Feature/VisualEditor/DynamicContentSourceTest.php
  • tests/Feature/VisualEditor/DynamicContentSourcesControllerTest.php
  • tests/Feature/VisualEditor/DynamicLoopBlockTest.php
  • tests/Feature/VisualEditor/RendererInlineTokensTest.php
  • tests/Feature/VisualEditor/SiteEditorFiltersTest.php
  • tests/Feature/VisualEditor/SnippetControllerTest.php
  • tests/Feature/VisualEditor/SnippetCycleGuardTest.php
  • tests/Feature/VisualEditor/VisibilityUsersSearchTest.php
  • tests/Support/FakeDynamicContentAccessor.php
  • tests/Support/FakeDynamicContentTypeRegistry.php
  • tests/Unit/Position/PositionEmitterTest.php
  • tests/Unit/Position/PositionResolverTest.php
  • tests/Unit/VisualEditor/Responsive/BreakpointRegistryTest.php
  • tests/Unit/VisualEditor/SiteEditor/Adapters/PatternAdapterTest.php
  • tests/Unit/VisualEditor/SiteEditor/Resolution/ResolvedPatternTest.php
  • tests/Unit/VisualEditor/Visibility/AuthRulesTest.php
  • tests/Unit/VisualEditor/Visibility/BrowserOsDeviceRuleTest.php
  • tests/Unit/VisualEditor/Visibility/HideRuleTest.php
  • tests/Unit/VisualEditor/Visibility/QueryStringRuleTest.php
  • tests/Unit/VisualEditor/Visibility/ReferrerRuleTest.php
  • tests/Unit/VisualEditor/Visibility/ScheduleRulesTest.php
  • tests/Unit/VisualEditor/Visibility/ScreenSizeRuleTest.php
  • tests/Unit/VisualEditor/Visibility/TreePrunerTest.php
  • tests/Unit/VisualEditor/Visibility/VisibilityEvaluatorTest.php

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Review on demand using usage pricing
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/1.4

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.

❤️ Share

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

@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in ArtisanPack UI Overview Jul 17, 2026
@ViewFromTheBox
ViewFromTheBox merged commit c7df1e3 into main Jul 17, 2026
11 checks passed
@ViewFromTheBox
ViewFromTheBox deleted the release/1.4 branch July 17, 2026 01:32
@github-project-automation github-project-automation Bot moved this from In Review to Done in ArtisanPack UI Overview Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment