Skip to content

feat: port crosswinds blocks under artisanpack/* (#495) - #577

Merged
ViewFromTheBox merged 37 commits into
release/1.1from
feature/495-port-crosswinds-blocks
Jun 13, 2026
Merged

ViewFromTheBox merged 37 commits into
release/1.1from
feature/495-port-crosswinds-blocks

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Jun 13, 2026 •

Copy link
Copy Markdown
Member

Description

Closes umbrella #495 — ports the legacy crosswinds-blocks set into the visual-editor package as first-party artisanpack/* blocks, mirroring the core-block fork pattern from #331. 25 blocks across 8 clusters (CW0–CW7), all with three-renderer parity (Blade + React + Vue) and renderer parity tests, plus two cross-cutting follow-ups (#565, #566) and a final inserter-icon + category cleanup pass.

Closes: #495, #496, #497, #498, #499, #500, #501, #502, #503, #565, #566

Type of Change

  • New feature (adds new functionality)
  • Enhancement (improves existing functionality)

Related Issue

Issue: #495

Motivation and Context

The crosswinds-blocks set was a useful starting point but never going to ship under its own name — bringing the blocks in-house as first-party artisanpack/* blocks gets them the parity guarantees, the renderer-bridge integration, the inserter polish, and the maintenance lifecycle the rest of the visual-editor package already has. Doing the port phase-by-phase (one cluster PR per CW#) kept reviews tractable; this umbrella PR is just the final merge of all of that into release/1.1 plus the inserter-icon + category cleanup we did at the end.

Changes Made

CW0 — pilot (#496, merged via #567)

  • artisanpack/breadcrumbs

CW1 — interactive cluster (#497, merged via #568)

  • artisanpack/accordions, artisanpack/accordion, artisanpack/accordion-title, artisanpack/accordion-body
  • artisanpack/tabs, artisanpack/tab-section

CW2 — grid cluster (#498, merged via #569)

  • artisanpack/grid (renamed from basic-grid)
  • artisanpack/grid-item (renamed from basic-grid-item)

CW3 — post navigation cluster (#499, merged via #570)

  • artisanpack/next-post, artisanpack/previous-post

CW4 — site chrome cluster (#500, merged via #571)

  • artisanpack/copyright, artisanpack/marquee, artisanpack/comments-number

CW5 — single-post content cluster (#501, merged via #572)

  • artisanpack/single-content, artisanpack/related-posts
  • artisanpack/author-social-icons, artisanpack/social-share-content

CW6 — generic search cluster (#502, merged via #573)

  • artisanpack/search-field, artisanpack/search-filters
  • artisanpack/search-filters-buttons, artisanpack/search-filters-taxonomy
  • artisanpack/post-types-search-results, artisanpack/single-post-types-search-results

CW7 — skills cluster (#503, merged via #574)

  • artisanpack/skills-slider

Cross-cutting follow-ups

Final cleanup (this PR's only direct commits)

  • Inserter icons. All 25 first-party blocks now use Font Awesome Free Solid SVG paths (copied verbatim from resources/icons/font-awesome/fas/, FA v7.2.0, CC BY 4.0). The hand-drawn approximations they replaced consistently read as amateur next to the Gutenberg core glyphs.
  • Categories. Every first-party block moved out of the catch-all artisanpack inserter category into a real WP one — text/media/design/widgets/theme — plus a new search category for the six search-cluster blocks. The 25 moves:
    • search (NEW): the 6 search-cluster blocks
    • design: accordion family (4), tabs family (2), grid + grid-item, callout, marquee
    • theme: breadcrumbs, comments-number, copyright, single-content, related-posts, author-social-icons, social-share-content
    • widgets: form, skills-slider
    • editor/custom-blocks.ts adds SEARCH_CATEGORY_SLUG + ensureSearchCategory(). ensureArtisanpackCategory() is retained for downstream apps but is no longer called at registration time.
  • NOTICE.md at the package root with the full Font Awesome attribution (Fonticons, Inc., CC BY 4.0, per-icon source URLs) and a per-icon credit table. Each inserter-icon.tsx header now references NOTICE.md + the CC BY 4.0 license URL.

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS (Darwin 25.5.0)
  • Browser: Safari (dev app served via Laravel Herd at https://artisanpack-ui-dev.test)
  • PHP Version: 8.4.3
  • Project Version: 1.1 (pre-release)

Tests Performed:

  1. npm test — full vitest run, all 2032 tests across 240 test files pass.
  2. npm run verify:parity — renderer parity script clean (151 blocks registered across Blade, React, and Vue).
  3. npm run build — production build succeeds.
  4. Manual editor verification in the dev app — opened the standalone editor (/admin/packages/visual-editor/standalone/editor), confirmed:
    • the new Search inserter category appears with the 6 search-cluster blocks
    • the 25 updated blocks render with their FA-based icons at the correct size in the inserter
    • the 25 blocks open and behave normally when inserted into the canvas
  5. Per-cluster PRs (feat: CW0 pilot — port crosswinds-blocks/breadcrumbs as artisanpack/breadcrumbs #567–feat: auto-inject custom block CSS into editor canvas iframe (#566) #576) each ran their own test + CodeRabbit cycles before merge.

Accessibility Tests Run

  • Keyboard navigation tested
  • Screen reader tested
  • Color contrast verified
  • ARIA labels checked

Details: Accessibility checks were run per cluster as part of the CW0–CW7 PR cycle. Each inserter-icon SVG carries aria-hidden="true" + focusable="false"; the block titles supply the inserter's accessible name. No focus-management or contrast regressions surfaced in this final cleanup.

Tests Added

  • Unit tests added/updated
  • Integration tests added/updated
  • All tests passing

Test details:

  • New ensureSearchCategory suite in editor/__tests__/custom-blocks.test.tsx mirrors the existing ensureArtisanpackCategory one.
  • Family contract tests updated for the category moves: search-cluster-family.test.ts (now search), single-post-content-family.test.ts (now theme), skills-slider.test.ts (now widgets), site-chrome-family.test.ts (now per-block: copyright=theme, marquee=design, comments-number=theme).
  • The bundled artisanpack/callout assertions in custom-blocks.test.tsx move from artisanpack to design.
  • Per-cluster tests added incrementally on each CW PR (block.json contracts, save shape, transforms, deprecations, three-renderer parity fixtures).

Documentation

  • Inline code documentation added
  • README updated
  • Wiki updated
  • API documentation updated

Documentation details: Inline comments updated on touched files. The new NOTICE.md documents the FA attribution. README / wiki updates for the new blocks will follow in a docs-only PR.

Screenshots

The 25 updated inserter icons can be spot-checked in the dev app at /admin/packages/visual-editor/standalone/editor — open the inserter and scroll through the Search / Design / Theme / Widgets categories.

Pre-Submission Checklist

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

Summary by CodeRabbit

  • New Features

    • Many new visual-editor blocks: accordions/tabs, breadcrumbs, grid + grid-item, marquee, skills slider, comments-number, author/social-share icons, search cluster (field/filters/taxonomy/buttons/results), single-content, related-posts, next/previous post navigation, copyright.
  • Style / Interactivity

    • New CSS families, responsive grid utilities, and a lightweight JS module enabling accordion and tabs behaviors.
  • Documentation

    • Added NOTICE and contributor guidance.
  • Tests

    • Extensive unit, feature and SSR-parity tests covering new blocks and behaviors.

ViewFromTheBox and others added 30 commits June 11, 2026 10:01
…readcrumbs

Forks the upstream crosswinds-blocks breadcrumbs block into the visual-editor
package and establishes the pattern for CW1–CW7 to follow:

- Editor scaffold under `resources/js/visual-editor/blocks/breadcrumbs/`
  (block.json, edit, save, index, inserter-icon, separators, css). Drops the
  upstream `breadrcumbsSchema` typo for `breadcrumbsSchema` since the fresh
  namespace has no legacy markup to preserve.
- Blade + React + Vue renderers reading a server-stamped `_resolvedTrail`
  attribute, emitting `<nav><ol>…</ol></nav>` with the selected chevron
  separator and optional schema.org BreadcrumbList microdata. URLs run
  through the existing UrlSanitizer / safeUrl helpers so unsafe schemes are
  dropped.
- Three-renderer parity tests (12 new assertions per renderer) covering
  schema-on/schema-off, separator fallback, empty trail, and unsafe-URL
  rejection. Manifest in `packages/renderer-parity.json` updated.

No transforms — upstream did not define any for breadcrumbs.

Follow-ups filed under umbrella #495:
- #565 — BreadcrumbsResolver to stamp `_resolvedTrail` on the server
- #566 — Editor canvas iframe pipeline for custom block CSS

Closes #496
- Only mark the breadcrumb span with `ap-breadcrumbs__current` when the
  trail entry is actually `current`. Previously every non-link branch
  carried the class — so an entry whose URL was sanitized away rendered
  as 'current' even when it wasn't the trail's final crumb. Mirrored
  across all three renderers (Blade, React, Vue).
- React: use the loop index for the `<li>` key instead of a
  position+label composite — labels can collide and the array index is
  already stable for this list.
- Add an assertion to the unsafe-URL parity tests confirming exactly one
  `ap-breadcrumbs__current` occurrence in the rendered output.
…adcrumbs

feat: CW0 pilot — port crosswinds-blocks/breadcrumbs as artisanpack/breadcrumbs
Adds the artisanpack/accordions, accordion, accordion-title,
accordion-body, tabs, and tab-section blocks plus three-renderer
parity (Blade + React + Vue) and renderer-parity tests.

Accordion family:
- accordions parent owns the wrapper; accordion child owns the
  panelId + panelIcon, auto-generated from clientId on mount.
- accordion-title / accordion-body are content-only — the accordion
  renderer walks its children and re-wraps them with the matching
  id / aria-controls / aria-labelledby / hidden wiring, so authors
  only ever set the panel id once.

Tabs family:
- tab-section owns its own label + tabId (both auto-fill on mount).
- tabs renderer walks innerBlocks to build the tablist AND re-wraps
  each section's grandchildren with the resolved tabId, so trigger
  aria-controls always lines up with panel id. Adds positional
  fallback for missing tabIds and dedup for duplicates.

Front-end:
- Per-family CSS + a vanilla interactivity script ship from
  visual-editor-renderer-blade under public/vendor/.../frontend/,
  wired into the new <x-ve-blocks-styles /> emission. The
  interactivity script handles accordion toggle (click + Enter /
  Space) and tab switching (click + arrow keys + Home / End).
- Editor canvas iframe also picks up the styles via canvas-styles
  with editor-only overrides that keep every panel / section
  expanded for editing.

Renderer infrastructure:
- BlockRenderer.php now passes the raw innerBlocks array to Blade
  partials (alongside the pre-rendered innerBlocksHtml) so partials
  can walk children when they need to derive parent state from them.

Closes #497
- Lowercase the `currentcolor` keyword in accordion + tabs CSS to
  satisfy stylelint's value-keyword-case rule.
- Slugify $tabId in the standalone tab-section partial so emitted
  id / aria-labelledby attributes are always valid HTML ids even
  when the persisted slug carries characters outside [a-z0-9-].
…-tabs

feat: port accordion + tabs block families
Forks `crosswinds-blocks/basic-grid` and `basic-grid-item` into the
artisanpack/* namespace with the "basic-" prefix dropped from block
names, attributes, classes, and labels.

Grid family:
- artisanpack/grid (parent) carries a single `numColumns` attribute,
  enrolled in artisanpackResponsive.attributes so the per-breakpoint
  HOC routes overrides into `responsive.numColumns.{bp}`. Gap is
  surfaced through the standard spacing.blockGap support with
  horizontal/vertical sides.
- artisanpack/grid-item (child) carries `gridColumnSpan`, `gridRowSpan`,
  and an `innerLayout` enum (normal/equal/center/bottom/last-bottom).
  Spans are enrolled in artisanpackResponsive.attributes; the
  column-span max is clamped to the grid's `numColumns` via block
  context.

Renderers (Blade + React + Vue):
- Per-breakpoint values encode as static classes:
    `ap-grid-has-N-{base|sm|md|lg|xl|2xl}-columns`
    `ap-grid-item-span-N-{bp}-{columns|row}`
- grid.css ships the matching @media-keyed rules; mobile-first cascade
  picks the active breakpoint's class at runtime. Class-driven (not
  per-instance @media via the accumulator) so the front-end works
  regardless of where `<x-ve-blocks-styles>` lands.

Editor canvas:
- canvas-styles.ts injects grid.css into the iframe so authors get an
  accurate preview as they switch breakpoints.

Tests:
- Renderer-parity tests cover the family in all three renderers
  (Unit) plus a Feature test that verifies the per-breakpoint class
  emission survives through `<x-ve-blocks>`.

Closes #498
- Move grid CSS link outside `$emitInteractive` gate — grid is a pure
  layout family with no companion interactivity script, so it must
  ship even when hosts opt out of accordion/tabs interactivity.
- Fix `.ap-grid-item-layout-bottom`: with `flex-direction: column` the
  main axis is vertical, so `justify-content: flex-end` (not
  `align-items: flex-end`) is what pushes children to the bottom.
- Validate raw `responsive.*` overrides as finite numbers before
  clamping in React + Vue renderers — previously non-numeric values
  silently snapped to the clamp min (1) and emitted bogus
  `ap-grid-*-1-{bp}-*` classes. Regression tests cover the fix.
…ds-blocks-cw2-grid-cluster

feat: port crosswinds-blocks grid + grid-item families (CW2)
Codifies the rule that blocks ported from the legacy upstream into the
artisanpack/* namespace must NOT carry any "CW#" / legacy-project-name
attribution in their code, comments, asset filenames, or commit
messages. Any Claude session opening this repo on any machine reads
the rule from here, so it is no longer dependent on per-machine
agent memory.
… blocks

Adds two adjacent-post container blocks that wrap inner post-* children
against the resolved neighbor in the same post type. PostResolver looks
up the adjacent post via the existing accessor candidates, recursively
re-stamps the inner-block subtree against the neighbor, and sets a
`_resolvedHasAdjacent` flag so renderers can collapse to empty markup
when no neighbor exists.

Also exposes the existing `isLink` / `linkTarget` / `rel` attributes on
`artisanpack/post-title` through a Settings sidebar panel so the titles
inside these wrappers can be made clickable — the Blade / React / Vue
renderers already honored the attributes; only the editor UI was
missing.

Closes #499
…-post-blocks

feat: artisanpack/next-post + artisanpack/previous-post container blocks
…me blocks

Adds three site-wide chrome blocks under the artisanpack namespace:

  - copyright (dynamic) — © + configurable text + current year, with three
    display modes (icon-text / icon-only / text-only). Year is read at
    render time on every renderer so the line stays accurate without a
    rebuild.
  - marquee (static) — RichText body wrapped in a sized `<div>`. The
    renderers emit an inline `animation: ap-marquee-scroll Ns linear
    infinite` declaration that drives the keyframe shipped in the new
    `frontend/marquee.css` asset; the editor preview drops the animation
    so authors can edit the text without it scrolling under their cursor.
  - comments-number (dynamic) — `<count> <singular|plural>` line.
    PostResolver stamps `_resolvedCommentCount` (reusing the existing
    branch shared with post-comments-count), and the renderers pick the
    singular or plural label by count.

Wires the marquee front-end stylesheet through `<x-ve-blocks-styles />`
(new `marqueeStyleHref` + `data-ve-marquee` link tag) so consumers ship
the keyframes alongside the existing accordion / tabs / grid bundles.

Closes #500
 - Honor prefers-reduced-motion on the marquee animation in both the
   editor preview and the published frontend stylesheet — the keyframe
   is suppressed and the off-screen transform is reset so opted-out
   visitors see static text instead of an endless scroll.
 - Extract the marquee width / speed clamp helpers into a shared
   `clamp.ts` module so edit.tsx and save.tsx can't drift in their
   validation ranges or defaults.
 - Freeze the system clock with `vi.useFakeTimers()` in the React + Vue
   copyright renderer specs so the year assertions stay deterministic
   across a Dec 31 → Jan 1 boundary.
 - Assert both `postId` AND `postType` in the comments-number
   usesContext contract test so a future removal of `postType` trips
   the suite.
…locks

feat: artisanpack/copyright + marquee + comments-number site-chrome blocks
…icons + social-share-content blocks

Ports the single-post content cluster under the artisanpack/* namespace
with full Blade + React + Vue renderer parity.

- single-content: container block scoped to one chosen entry. QueryInliner
  resolves the post via QueryResolverContract and re-stamps inner blocks
  via PostResolver, falling back to the host post when postId is 0.
- related-posts: loop block over N taxonomy-related posts. QueryInliner
  derives the taxonomy term IDs from the host post (now threaded through
  inline()) and clones the inner-block tree once per result with
  PostResolver stamps.
- author-social-icons: PostResolver stamps _resolvedAuthorSocialLinks
  from the author's stored profile URLs. Email bare addresses normalize
  to mailto: URLs.
- social-share-content: PostResolver builds per-platform share URLs from
  the host post's permalink, title, and featured image and stamps
  _resolvedShareLinks.

Renderer-parity manifest, three-renderer parity fixtures, block.json
contract test, PostResolver social-icon test, and QueryInliner single-
post-content test added. CLAUDE.md carries a temporary in-progress
section so the work can be picked up from another machine.

Refs #501

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Manual-testing follow-ups on the single-post content cluster:

- single-content: replace the post-id text input with a searchable
  post dropdown backed by /visual-editor/api/{plural}, plus inserter
  variations registered per cms-framework content type via a new
  data-content-types attribute on the mount element. Variation slugs
  are singular (post/page) to match the WP entity convention; the
  paired plural drives the REST URL. QueryInliner singularizes any
  legacy plural postType so blocks saved before this pass still
  resolve.
- related-posts: mirror the query block's attribute shape (query
  object + queryId + displayLayout) and inspector UX (perPage /
  columns / offset / orderBy controls). The canvas now fetches and
  renders N-1 read-only preview cards below the editable inner
  template so authors can see the actual count.
- social-share-content + author-social-icons: icon color, hover icon
  color, icon background color, and hover icon background color via
  PanelColorSettings; renderers emit only CSS custom properties so
  the :hover swap actually applies (inline color would have beaten
  the :hover rule on specificity). Switched glyph paths to Simple
  Icons (CC0) for a polished brand look; email + website are
  matching hand-drawn UI glyphs since Simple Icons is brands-only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Escape the content-types JSON in editor.blade.php + mount.blade.php
  via JSON_HEX_* so attribute injection can never break out of the
  data-content-types value.
- Swap the hand-rolled singularizer in VisualEditorComponent +
  routes/web.php for Str::singular so legitimate stems like `types`
  or `classes` aren't mangled.
- Drop dangerouslySetInnerHTML from the related-posts editor preview;
  excerpts get tag-stripped client-side before render.
- Validate the single-content collection slug against [a-z0-9_-] and
  fall back to `posts` when it isn't recognised + isn't slug-shaped,
  so a tampered `postType` can't compose a path-traversal URL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Set aria-label on every social-share + author-social chip anchor so
  icon-only modes still expose a discernable name to screen readers.
  Vue inserts the attribute after `style` to keep React/Vue parity on
  the byte-compared HTML the renderer-parity harness checks.
- Switch the CSS `fill: currentColor` declarations to lowercase
  `currentcolor` in both copies of social-icons.css to satisfy the
  project's stylelint value-keyword-case rule.
- Replace the leftover hand-rolled plural-strip in
  QueryInliner::expandSingleContent with Str::singular so the back-compat
  path uses the same inflector as the variations registry (avoids the
  `types` -> `typ` class of mistakes).
- Capitalize "Mac mini" in CLAUDE.md per Apple style.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ontent-cluster

feat(#501): single-post content cluster — single-content, related-posts, social-share + author-social icons
Forks six search-family blocks under `artisanpack/*` with full Blade +
React + Vue parity: `search-field`, `search-filters`,
`search-filters-buttons`, `search-filters-taxonomy`,
`post-types-search-results`, `single-post-types-search-results`. Each
block ships block.json + dynamic save, edit-side preview, inserter icon,
and renderer partials/components. Inter-block query state flows through
the standard GET form (hidden `post_type` + `name="s"` + `?{taxonomy}=`
selects) so authors can wire the cluster end-to-end with no extra
JavaScript. Renderer-side request state arrives via `_resolved*`
attribute stamps; Blade falls back to the live request and cms-framework
`PostCategory` / `PostTag` models when the host has not pre-resolved.

Adds 17 new family-contract tests + 7 React/Vue parity fixtures (full
suites: 2018 JS + 1078 PHP). Renderer-parity manifest extended with the
six new names.

Also fixes two pre-existing bugs the new templates surfaced:
- `__('Pagination')` in `query-pagination.blade.php` matched Laravel's
  bundled `lang/en/pagination.php`, returning the file's `previous` /
  `next` array and crashing `e()` downstream. Renamed the aria-label
  key to `'Pagination navigation'` with an inline note explaining the
  collision so a future contributor does not reintroduce it.
- Blade's directive lexer does not recognize `@endif` when it sits
  directly after a non-whitespace character (`selected@endif`), so the
  literal `@endif` leaked into the compiled output and unbalanced the
  `<?php if(...): ?>`. Replaced the inline-attribute pattern with a
  `@php`-block flag plus `{!! $flag ? ' selected' : '' !!}`.

Closes #502
- Sync `query-pagination` aria-label across React + Vue renderers
  (both were still emitting "Pagination" while Blade was renamed to
  "Pagination navigation" to dodge the lang/en/pagination.php collision).
- Localize `search-filters-buttons` Blade fallbacks via `__()`.
- Derive `search-filters-taxonomy` `$taxonomyName` from the sanitized
  slug when the author has not supplied one (`post_tag` → `Post Tag`).
- Suffix `search-filters-taxonomy` `$selectId` with an attributes hash so
  two instances of the same taxonomy on one page get distinct ids.
- Normalize the incoming `?post_type=` value in
  `single-post-types-search-results` the same way the saved attribute
  is sanitized so `?post_type=Post` matches a `post` section.
- Use React `useId()` for the preview ids in `search-field/edit.tsx`
  and `search-filters-taxonomy/edit.tsx` so multiple instances no
  longer collide on duplicate DOM ids.
…uster

feat: port crosswinds search cluster (#502)
Static progress-bar block that visualises a skill level as a
horizontal filled bar. Three-renderer parity (Blade + React + Vue)
with shared clamp helpers, ARIA progressbar semantics, and a
strict allow-list for inline CSS colour values on the Blade side.

No `skill` CPT dependency — the block ships as a manual-input
control (skill level, bar height, colours, accessible label), so
authors can render it without registering an upstream post type.

Closes #503
Mirror the Blade-side colour allow-list in the React + Vue renderers so
the same saved block renders identical colours across all three.
Renderers now reject anything that isn't a hex / rgb / rgba / hsl /
hsla value, matching the Blade contract.
…ider

feat: add artisanpack/skills-slider block
Stamps `_resolvedTrail` on every `artisanpack/breadcrumbs` block in the
saved tree so the Blade / React / Vue renderers (which already consume
the attribute) emit a populated `<ol>` on the public frontend.

The default trail is `Home → …ancestors → current`. The Home hop reads
from `config('artisanpack.visual-editor.breadcrumbs.home_url')` (falls
back to `url('/')`); the last entry is auto-marked `current: true` so
the renderer drops the link and emits `aria-current="page"`. Hierarchical
posts (pages with `parent`/`parent_id`) walk the ancestor chain bounded
to 32 levels with cycle detection.

Hosts customize the trail through the `ap.visual-editor.breadcrumbs.trail`
filter (e.g. to insert a "Category" hop between Home and a blog post)
without subclassing. A pre-stamped `_resolvedTrail` on the saved tree
wins over the resolver fallback, matching PostResolver's contract.

Also ships `breadcrumbs.css` to the frontend asset bundle and wires it
into `<x-ve-blocks-styles />` so the inline-flex layout + chevron
separator applies on the public site without each host re-implementing
the rules. Tightens the inter-item gap from 0.5rem to 0.25rem.

Closes #565

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove the redundant `onHomepage()` short-circuit in
`BreadcrumbsResolver::defaultTrail()` — both the homepage-specific and
the generic `null === $post` branches returned the same trail, so the
extra check (and its `onHomepage()` helper + `Illuminate\Http\Request`
import) were dead surface. Behavior unchanged: a null post still
collapses to a single Home entry that the finalizer marks
`current: true`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…esolver

feat: add BreadcrumbsResolver for artisanpack/breadcrumbs (#565)
The sandboxed BlockCanvas iframe never saw the per-block stylesheets
imported via side effect from each block's index.ts, so blocks with
their own CSS (callout, breadcrumbs, and future blocks) rendered
unstyled in the editor preview.

Resolve every blocks/*/*.css file as inline CSS via import.meta.glob
and append the discovered sheets to canvasStyles, replacing the five
per-family manual imports that used to force an edit to canvas-styles
every time a new custom block landed. Entries are sorted by path so
the cascade is deterministic across builds.

Drop the inline style={} workaround from blocks/breadcrumbs/edit.tsx
now that breadcrumbs.css reaches the iframe, and extend the canvas-
styles test suite to assert the glob actually discovers the cited
sheets (callout, breadcrumbs, accordion, tabs, grid, marquee, shared
social-icons) rather than snapshotting a single file count.

Closes #566
Drop the identity `.map((path) => path)` when constructing the
blockStyleStrings Set in the canvas-styles test — the array is
already a string[], so it can flow straight into `new Set(...)`.
…-custom-block-css

feat: auto-inject custom block CSS into editor canvas iframe (#566)
Move every first-party block out of the catch-all `artisanpack` inserter
category into the standard WP categories (text/media/design/widgets/theme)
plus a new `search` category for the six search-cluster blocks. The
`artisanpack` category was never going to feel native next to `core/*`
blocks — the new homes match how authors actually scan the inserter.

Category moves (25 blocks):

  search   (new)  search-field, search-filters, *-filters-buttons,
                  *-filters-taxonomy, post-types-search-results,
                  single-post-types-search-results
  design          accordion, accordion-body, accordion-title, accordions,
                  tabs, tab-section, grid, grid-item, callout, marquee
  theme           breadcrumbs, comments-number, copyright, single-content,
                  related-posts, author-social-icons, social-share-content
  widgets         form, skills-slider

Editor wiring:

  - editor/custom-blocks.ts adds SEARCH_CATEGORY_SLUG +
    ensureSearchCategory() and calls it from registerCustomBlocks().
    ensureArtisanpackCategory() is retained for downstream apps that
    still ship blocks under the `artisanpack` slug but is no longer
    called at registration time.
  - 8 block index.ts files (accordion x4, tabs x2, grid x2) now wire
    their new inserter-icon module through to registerBlockType.

Inserter icons — switch all 25 first-party blocks to Font Awesome Free
Solid SVGs (path data copied verbatim from the bundled FA SVGs under
resources/icons/font-awesome/fas, CC BY 4.0). Replaces the hand-drawn
SVGs that were ~close to the Gutenberg core glyphs but consistently
read as amateur next to them:

  accordion              -> square-caret-down
  accordion-body         -> align-justify
  accordion-title        -> heading
  accordions             -> layer-group
  author-social-icons    -> id-card-clip
  breadcrumbs            -> signs-post
  callout                -> lightbulb
  comments-number        -> comment-dots
  copyright              -> copyright
  form                   -> clipboard-list
  grid                   -> table-cells
  grid-item              -> square
  marquee                -> bullhorn
  post-types-search-results        -> magnifying-glass-arrow-right
  related-posts          -> newspaper
  search-field           -> magnifying-glass
  search-filters         -> filter
  search-filters-buttons -> sliders
  search-filters-taxonomy-> tags
  single-content         -> file-lines
  single-post-types-search-results -> note-sticky
  skills-slider          -> bars-progress
  social-share-content   -> share-nodes
  tab-section            -> window-maximize
  tabs                   -> window-restore

Tests:

  - search-cluster-family / single-post-content-family / skills-slider
    assert their new categories.
  - site-chrome-family becomes per-block (copyright=theme,
    marquee=design, comments-number=theme).
  - custom-blocks.test.tsx adds an ensureSearchCategory suite mirroring
    the existing ensureArtisanpackCategory one; the callout block.json
    + registerCustomBlocks assertions move to the `design` category.

2032/2032 JS tests pass; renderer parity OK.
Add NOTICE.md at the package root with full Font Awesome attribution
for the bundled SVG paths now used by every first-party inserter icon:
credits Fonticons, Inc., links to the FA Free license and CC BY 4.0
license text, and lists which icon each of the 25 blocks pulls from.
The 25 per-file inserter-icon.tsx comments are updated in turn to
reference NOTICE.md and the CC BY 4.0 license URL so each consuming
file carries its own attribution trail.

Skipped CodeRabbit's other findings (one per CR pass): each claimed
the SVG path data for `share-nodes` / `magnifying-glass` was wrong
and should be swapped for the FA v6 path with viewBox "0 0 448 512".
This package ships FA v7.2.0 (viewBox "0 0 512 512") — the paths in
the inserter components match the bundled `.svg` source files
under `resources/icons/font-awesome/fas/` byte-for-byte. The
suggestions were CR confusing a different FA major version for the
canonical one.

Tests: 2032/2032 JS tests still pass.
@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 63372dbb-3d2a-439f-ac50-846ba3f8369d

📥 Commits

Reviewing files that changed from the base of the PR and between c9182c1 and 35e49f1.

📒 Files selected for processing (3)
  • config/visual-editor.php
  • resources/js/visual-editor/editor/content-type-registry.ts
  • tests/Unit/VisualEditor/EnabledBlockNamesTest.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • config/visual-editor.php
  • tests/Unit/VisualEditor/EnabledBlockNamesTest.php
  • resources/js/visual-editor/editor/content-type-registry.ts

📝 Walkthrough

Walkthrough

This PR ports multiple artisanpack/* block families into the visual editor across WordPress block definitions, Blade/React/Vue renderers, PHP resolvers and query inlining, frontend assets, editor wiring, parity registration, and test coverage, while also adding breadcrumbs configuration, content-type discovery, search category registration, and notice documentation.

Changes

ArtisanPack block port

Layer / File(s) Summary
Project wiring and shared foundations
CLAUDE.md, NOTICE.md, config/visual-editor.php, packages/renderer-parity.json, resources/js/visual-editor/editor/*, resources/views/components/editor.blade.php, resources/views/editor/mount.blade.php, routes/web.php, resources/js/visual-editor/blocks/_shared/*, resources/js/visual-editor/blocks/callout/*, resources/js/visual-editor/blocks/form/*
Adds project guidance, third-party notice text, breadcrumbs config, renderer parity entries, search category and content-type wiring, shared social helpers/styles, auto-discovered canvas block CSS, and category/icon metadata updates.
WordPress block definitions and editor UIs
resources/js/visual-editor/blocks/accordion*/*, .../tabs*/*, .../grid*/*, .../breadcrumbs/*, .../comments-number/*, .../copyright/*, .../marquee/*, .../next-post/*, .../previous-post/*, .../single-content/*, .../related-posts/*, .../author-social-icons/*, .../social-share-content/*, .../search-*/*, .../post-types-search-results/*, .../single-post-types-search-results/*, .../skills-slider/*, resources/js/visual-editor/blocks/post-title/edit.tsx, resources/js/visual-editor/blocks/__tests__/*, resources/js/visual-editor/blocks/_shared/__tests__/*
Adds block metadata, editor UIs, save behavior, inserter icons, local styles, single-content variations, post-title link settings, and editor-side block family tests for the new block clusters.
Backend resolution and asset component plumbing
src/Resources/PostResolver.php, src/Resources/QueryInliner.php, src/View/Components/VisualEditorComponent.php, packages/visual-editor-renderer-blade/src/{BlockRenderer.php,Resolvers/BreadcrumbsResolver.php,Support/SocialIconRegistry.php,View/Components/*,VisualEditorRendererBladeServiceProvider.php}
Extends PHP resolution to stamp breadcrumbs, adjacent-post state, comments count, author/share social links, single-content, and related-post iterations, while passing inner block trees and new asset URLs through Blade renderer components.
Blade block rendering and frontend assets
packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/*, packages/visual-editor-renderer-blade/resources/views/components/blocks-styles.blade.php, packages/visual-editor-renderer-blade/resources/assets/frontend/*
Adds Blade markup, styles, and browser interactivity for breadcrumbs, accordion/tabs, grid, marquee, comments-number, copyright, adjacent-post wrappers, single-content, related posts, social chips, search filters, and skills slider.
React runtime renderers and registration
packages/visual-editor-renderer-react/src/blocks/artisanpack/*, packages/visual-editor-renderer-react/src/support/socialColorStyle.ts, packages/visual-editor-renderer-react/src/blocks/registerCoreBlocks.ts, packages/visual-editor-renderer-react/tests/BlockTree.test.tsx
Adds React renderers and registration for the same block families, plus shared social color helpers and renderer tests for normalized output and conditional rendering.
Vue runtime renderers and parity
packages/visual-editor-renderer-vue/src/blocks/artisanpack/*, packages/visual-editor-renderer-vue/src/support/socialColorStyle.ts, packages/visual-editor-renderer-vue/src/blocks/registerCoreBlocks.ts, packages/visual-editor-renderer-vue/tests/{BlockTree.test.ts,parity.test.ts}
Adds Vue renderers and registration for the new block families, shared social color helpers, Vue renderer tests, and expanded React/Vue SSR parity fixtures.
PHP resolver and Blade validation tests
packages/visual-editor-renderer-blade/tests/Feature/*, packages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.php, tests/Unit/VisualEditor/Resources/*, tests/Unit/VisualEditor/RegisterBlockTest.php
Adds PHP feature and unit coverage for breadcrumbs resolution, responsive grid rendering, Blade block output, adjacent-post stamping, comments count stamping, social link stamping, and single-content/related-post query inlining behavior.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

Poem

A rabbit hopped through blocks all day,
stitching trails and tabs in tidy array.
With grids and chips and sliders bright,
the renderers now all nibble in sync tonight.
🐇✨ Carrots for every passing test!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/495-port-crosswinds-blocks

@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in ArtisanPack UI Overview Jun 13, 2026
@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review June 13, 2026 01:55
CI surfaced this when the umbrella PR ran the full suite for the first
time (the per-cluster PRs only ran CodeRabbit). The assertion still
expected `artisanpack/callout` to live under the `artisanpack` inserter
category, but the icon + recategorise commit moved it to `design`
alongside the rest of the design-cluster blocks.
@ViewFromTheBox

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ViewFromTheBox

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (14)
packages/visual-editor-renderer-vue/src/blocks/artisanpack/searchCluster.ts-72-74 (1)

72-74: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use an instance-unique seed for generated control IDs.

Line 72 and Line 178 derive IDs from attribute content only, so two identical block instances can emit the same id. That can break label[for] targeting and produce invalid duplicate IDs in the same document. Please include a per-instance unique seed (block/node identity) when composing these IDs.

Also applies to: 178-179

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/visual-editor-renderer-vue/src/blocks/artisanpack/searchCluster.ts`
around lines 72 - 74, The generated control IDs (e.g., inputId produced via
shortHash(`${label}|${placeholder}|${searchValue}`)) only use attribute content
so identical blocks collide; include a per-instance unique seed when composing
these IDs (for example `${instanceSeed}|${label}|${placeholder}|${searchValue}`)
and persist that seed across renders (use a stable node/block id if available or
generate-and-store one on mount/useRef) so IDs remain unique and stable; apply
this change to the inputId construction shown and the analogous ID generation at
the other site (the occurrence referenced at lines 178-179) so both places use
the same per-instance seed.
packages/visual-editor-renderer-vue/src/blocks/artisanpack/breadcrumbs.ts-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove internal phase-label wording from the header comment.

Line 2 includes an internal phase label in source comments; this should be removed to match repository porting conventions.

Suggested edit
- * Vue renderer for the `artisanpack/breadcrumbs` block (CW0 pilot — `#496`).
+ * Vue renderer for the `artisanpack/breadcrumbs` block (`#496`).

Based on learnings, internal phase labels should not appear in code/comments during these ports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/visual-editor-renderer-vue/src/blocks/artisanpack/breadcrumbs.ts` at
line 2, The header comment for the Vue renderer of the `artisanpack/breadcrumbs`
block contains an internal phase label ("CW0 pilot — `#496`"); remove that phrase
so the comment reads simply "Vue renderer for the `artisanpack/breadcrumbs`
block." Locate the top-of-file header comment in the breadcrumbs renderer and
delete the internal phase-label wording, leaving the rest of the comment intact.

Source: Learnings

packages/visual-editor-renderer-react/src/blocks/artisanpack/breadcrumbs.tsx-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove internal phase label from header comment.

Line 2 uses CW0 pilot, which violates the repo rule against internal phase labels in code/comments.

Suggested edit
- * React renderer for the `artisanpack/breadcrumbs` block (CW0 pilot — `#496`).
+ * React renderer for the `artisanpack/breadcrumbs` block (`#496`).

Based on learnings: “Do NOT reference internal phase labels (CW0, CW1, CW2, etc.) … in code, comments, asset filenames, class/property names, CSS comments, test descriptions, or commit messages.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/visual-editor-renderer-react/src/blocks/artisanpack/breadcrumbs.tsx`
at line 2, Remove the internal phase label "CW0 pilot" from the header comment
in the artisanpack/breadcrumbs renderer; edit the header comment that currently
reads "React renderer for the `artisanpack/breadcrumbs` block (CW0 pilot —
`#496`)" to a neutral form such as "React renderer for the
`artisanpack/breadcrumbs` block" so no internal phase labels remain; ensure no
other occurrences of "CW0", "CW1", etc. remain in that header comment or nearby
comments.

Source: Learnings

packages/visual-editor-renderer-react/src/blocks/artisanpack/searchCluster.tsx-67-69 (1)

67-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Ensure control IDs are unique per block instance.

Line 67 and Line 145 derive IDs from repeatable values, so identical instances can emit duplicate IDs. That breaks label targeting and creates invalid DOM semantics on pages with multiple instances. Please include an instance-unique suffix for both IDs (resolver/client instance identifier), not only value-derived hashes.

Also applies to: 145-146

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/visual-editor-renderer-react/src/blocks/artisanpack/searchCluster.tsx`
around lines 67 - 69, The IDs computed from shortHash (e.g., the inputId
constant created with shortHash(`${label}|${placeholder}|${searchValue}`)) are
only value-derived and can collide across repeated block instances; include a
per-instance unique suffix (for example a renderer/instance identifier passed as
a prop or obtained from context like instanceId or rendererInstanceId) when
constructing these IDs so they are unique per block instance. Update the inputId
expression to append or incorporate that instance identifier (use the same
instance identifier consistently wherever related IDs are computed), and make
the same change to the other ID(s) derived from values later in the file (the ID
at the 145–146 site mentioned in the review) so label/aria relationships remain
correct across multiple instances. Ensure the instance identifier is
required/validated or generated once per block instance and reused for all IDs
in this component.
packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/author-social-icons.blade.php-124-124 (1)

124-124: ⚠️ Potential issue | 🟡 Minor

Localize the ARIA label using a translatable %s placeholder
Line 124 hardcodes 'Author on ' . $chip['label']; the repo’s dynamic i18n strings use sprintf( __( '...%s' ), ... ) rather than __(..., [ ... ]).

💡 Suggested change
-			<a class="ap-author-social-icons__chip {{ $chip['slug'] }}" href="{{ $chip['url'] }}" aria-label="{{ 'Author on ' . $chip['label'] }}"{!! $styleAttr !!}>
+			<a class="ap-author-social-icons__chip {{ $chip['slug'] }}" href="{{ $chip['url'] }}" aria-label="{{ sprintf( /* translators: %s: author label. */ __( 'Author on %s' ), $chip['label'] ) }}"{!! $styleAttr !!}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/author-social-icons.blade.php`
at line 124, Replace the hardcoded concatenation used for the aria-label on the
anchor with a translatable sprintf-style string: change the aria-label
expression on the ap-author-social-icons__chip anchor to use sprintf( __(
'Author on %s' ), $chip['label'] ) (inside Blade echoing) so the label becomes
translatable via the %s placeholder while preserving the existing $chip['label']
and $styleAttr.
packages/visual-editor-renderer-blade/src/Resolvers/BreadcrumbsResolver.php-232-236 (1)

232-236: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use strict boolean normalization for current in trail entries.

!empty() makes string values like "false" truthy, which can incorrectly mark breadcrumbs as current when filters pass scalar strings.

Suggested fix
-			$normalized[] = [
+			$currentRaw = $entry['current'] ?? false;
+			$current    = is_bool( $currentRaw )
+				? $currentRaw
+				: filter_var( $currentRaw, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
+
+			$normalized[] = [
 				'label'   => $label,
 				'url'     => '' === $url ? null : $url,
-				'current' => ! empty( $entry['current'] ),
+				'current' => true === $current,
 			];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/visual-editor-renderer-blade/src/Resolvers/BreadcrumbsResolver.php`
around lines 232 - 236, The 'current' normalization uses "! empty(
$entry['current'] )" which treats strings like "false" as truthy; update the
BreadcrumbsResolver normalized array assignment so the 'current' value is
strictly coerced to boolean (e.g. use filter_var($entry['current'],
FILTER_VALIDATE_BOOLEAN) or an explicit boolean check) instead of !empty(),
replacing the existing 'current' => ! empty( $entry['current'] ) expression in
the block that builds $normalized[].
packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/accordion.blade.php-6-20 (1)

6-20: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize panelId to a valid HTML id token before wiring ARIA/data attributes.

Escaping protects against injection, but it does not prevent invalid id characters (for example spaces), which can break control linkage and selector-based interactivity.

Suggested fix
-	$panelId = isset( $attributes['panelId'] ) ? (string) $attributes['panelId'] : '';
+	$panelId = isset( $attributes['panelId'] ) ? trim( (string) $attributes['panelId'] ) : '';
+	$panelId = preg_replace( '/[^A-Za-z0-9_.:-]+/', '-', $panelId ) ?? '';
+	$panelId = trim( $panelId, '-.' );
packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-filters.blade.php-14-16 (1)

14-16: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Re-apply safe defaults when sanitized slugs become empty.

After regex sanitization, an all-invalid input can become ''; current code then emits empty query keys/values instead of a usable default.

  • packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-filters.blade.php#L14-L16: if $postType === '' after sanitize, reset to 'post' before rendering the hidden input.
  • packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-filters-taxonomy.blade.php#L19-L21: if $taxonomy === '' after sanitize, reset to 'category' before using it for request lookup and <select name>.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-filters.blade.php`
around lines 14 - 16, In search-filters.blade.php (lines 14-16) the sanitized
$postType can become empty; after the preg_replace call, check if $postType ===
'' and if so set it back to 'post' before rendering the hidden input. In
search-filters-taxonomy.blade.php (lines 19-21) do the same for $taxonomy: after
sanitization check if $taxonomy === '' and if so reset it to 'category' before
using it for request lookups and as the <select name> value; no other changes
are required at the sibling site.
src/View/Components/VisualEditorComponent.php-66-69 (1)

66-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

PHPDoc missing plural key in content-type shape.

The property type declares array{slug: string, label: string}, but resolveContentTypes() (lines 138–145) returns array{slug: string, plural: string, label: string}. Update both PHPDoc blocks to match the actual shape.

📝 Proposed fix
 /**
- * `@var` array<int, array{slug: string, label: string}>
+ * `@var` array<int, array{slug: string, plural: string, label: string}>
  */
 public array $contentTypes;

and

 /**
  * Lists the registered cms-framework content types so the editor can
  * surface variations of `artisanpack/single-content` (one per type)
  * and any other block that wants to enumerate available resources.
  *
- * `@return` array<int, array{slug: string, label: string}>
+ * `@return` array<int, array{slug: string, plural: string, label: string}>
  *
  * `@since` 1.0.0
  */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/View/Components/VisualEditorComponent.php` around lines 66 - 69, The
PHPDoc for the property $contentTypes and the PHPDoc for the return shape of
resolveContentTypes() are out of sync with actual data (they omit the "plural"
key); update both PHPDoc annotations to use array<int, array{slug: string,
plural: string, label: string}> so the declared shape matches the array returned
by resolveContentTypes().
resources/js/visual-editor/blocks/single-content/variations.ts-23-24 (1)

23-24: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize variation labels/descriptions before shipping.

These inserter-visible strings are hardcoded in English, so they won’t translate in localized editor builds.

Suggested fix
 import type { BlockConfiguration } from '`@wordpress/blocks`';
+import { __, sprintf } from '`@wordpress/i18n`';

 import { getContentTypes } from '../../editor/content-type-registry';
+import { TEXT_DOMAIN } from '../../vendor/i18n';
@@
         name: `single-${type.slug}`,
-        title: `Single ${type.label}`,
-        description: `Render a single ${type.label.toLowerCase()} and scope the inner blocks to it.`,
+        title: sprintf(__('Single %s', TEXT_DOMAIN), type.label),
+        description: sprintf(
+            __('Render a single %s and scope the inner blocks to it.', TEXT_DOMAIN),
+            type.label.toLowerCase()
+        ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/visual-editor/blocks/single-content/variations.ts` around lines
23 - 24, Localize the inserter-visible strings in variations.ts by replacing the
hardcoded title and description with i18n calls: use sprintf( __( 'Single %s',
'your-text-domain' ), type.label ) for title and sprintf( __( 'Render a single
%s and scope the inner blocks to it.', 'your-text-domain' ),
type.label.toLowerCase() ) for description (or use __ with concatenation if
preferred), and ensure you import/wp.i18n functions (__, sprintf) at the top of
the file; update the title and description properties accordingly so they are
translatable.
resources/js/visual-editor/blocks/skills-slider/edit.tsx-131-135 (1)

131-135: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align the ARIA range with the actual control bounds.

skillLevel is clamped to 1–100 and the inspector only allows 1–100, but aria-valuemin still advertises 0. That makes the announced range inconsistent with the value users can actually set.

♻️ Proposed fix
-                    aria-valuemin={0}
+                    aria-valuemin={1}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/visual-editor/blocks/skills-slider/edit.tsx` around lines 131 -
135, The ARIA range advertised by the progressbar is inconsistent: aria-valuemin
is set to 0 while the control clamps skillLevel to 1–100 (safeLevel) and the
inspector only allows 1–100; update the aria-valuemin attribute to match the
actual minimum (set aria-valuemin={1} or use the existing min constant/prop if
one exists) in the element that renders role="progressbar" (the same JSX that
sets aria-valuenow={safeLevel} and aria-label={ariaLabel || __('Skill level',
TEXT_DOMAIN)}), so the announced range aligns with the real control bounds.
resources/js/visual-editor/blocks/breadcrumbs/index.ts-5-6 (1)

5-6: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove internal phase/provenance wording from this docblock.

Line 5 still includes internal rollout/provenance labeling that should not ship in source comments.

Based on learnings: internal phase labels and legacy provenance naming should be excluded from code/comments during this porting work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/visual-editor/blocks/breadcrumbs/index.ts` around lines 5 - 6,
Remove the internal rollout/provenance wording from the top docblock that
mentions `@wordpress/blocks.registerBlockType`—edit the comment to strip any
internal phase labels (e.g., "CW0 pilot", "provenance", or similar
rollout/legacy tags) so only neutral, public-facing documentation remains; leave
the factual description of the block and its behavior but remove internal
provenance phrases.

Source: Learnings

resources/js/visual-editor/blocks/breadcrumbs/edit.tsx-73-76 (1)

73-76: ⚠️ Potential issue | 🟡 Minor

Use literal strings in __() for SelectControl option labels
In resources/js/visual-editor/blocks/breadcrumbs/edit.tsx and resources/js/visual-editor/blocks/copyright/edit.tsx, __() is called with option.label (a computed/member value), which i18n extraction won’t pick up reliably. Build the options so the first argument to __() is a static string literal (e.g., __('<label literal>', TEXT_DOMAIN)), not __({}[key], TEXT_DOMAIN).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/visual-editor/blocks/breadcrumbs/edit.tsx` around lines 73 - 76,
The SelectControl options use __() with a computed value (option.label) so i18n
extraction will miss strings; update the options construction so the first
argument to __() is a static literal for each label instead of
__((option.label), TEXT_DOMAIN). Concretely, replace the current
SEPARATOR_OPTIONS.map(...) usage in the options prop with either (a) prebuild
SEPARATOR_OPTIONS so each entry's label is already the result of a literal call
like __('Slash', TEXT_DOMAIN) and then use that label directly, or (b) map using
a switch/lookup that passes a string literal into __() for each known option
(e.g., __('Slash', TEXT_DOMAIN), __('Arrow', TEXT_DOMAIN), etc.), keeping value
as option.value; ensure the code references SEPARATOR_OPTIONS and TEXT_DOMAIN so
the correct symbols are updated.
resources/js/visual-editor/blocks/search-filters/edit.tsx-62-69 (1)

62-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the default post option selectable.

The fallback only runs when the registry is empty. If the registry contains other types but not post, this control renders with value="post" and no matching option, so the editor shows a blank selection.

♻️ Proposed fix
-    if (options.length === 0) {
-        options.push({ value: 'post', label: __('Posts', TEXT_DOMAIN) });
-    }
+    if (!options.some(({ value }) => value === 'post')) {
+        options.unshift({ value: 'post', label: __('Posts', TEXT_DOMAIN) });
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/visual-editor/blocks/search-filters/edit.tsx` around lines 62 -
69, The options array built from getContentTypes() can lack a 'post' entry while
the UI still uses value='post', causing an unmatched blank selection; update the
logic that creates options (the options variable) to ensure a selectable default
by checking if options.some(o => o.value === 'post') and if false add { value:
'post', label: __('Posts', TEXT_DOMAIN) } (use push or unshift as desired) so
'post' is present whenever missing.
🧹 Nitpick comments (4)
packages/visual-editor-renderer-vue/tests/BlockTree.test.ts (1)

576-577: ⚡ Quick win

Remove internal phase-label wording from the test title.

Line 576 includes "(I4 fork)", which conflicts with the repository’s naming guidance for tests/comments. Please use a neutral description instead.

Suggested edit
-    it('carries the `#338` a11y fix forward to artisanpack/search (I4 fork)', () => {
+    it('carries the `#338` a11y fix forward to artisanpack/search', () => {

Based on learnings: “Do NOT reference internal phase labels ... in ... test descriptions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/visual-editor-renderer-vue/tests/BlockTree.test.ts` around lines 576
- 577, Update the test title string in the it(...) call that currently reads
"carries the `#338` a11y fix forward to artisanpack/search (I4 fork)" by removing
the internal phase label "(I4 fork)" and replacing it with a neutral description
(e.g., "carries the `#338` a11y fix forward to artisanpack/search"); leave the
test body and makeBlock(...) invocation unchanged.

Source: Learnings

packages/visual-editor-renderer-blade/tests/Feature/BlocksStylesComponentTest.php (1)

24-42: ⚡ Quick win

Add an explicit assertion that interactivity.js is not emitted when :interactive="false".

Current expectations verify independent CSS emission but do not protect the negative contract for script emission.

💡 Suggested test addition
 expect( $rendered )
 	->toContain( '/vendor/visual-editor-renderer-blade/frontend/grid.css' )
 	->toContain( 'data-ve-grid' )
 	->toContain( '/vendor/visual-editor-renderer-blade/frontend/marquee.css' )
 	->toContain( 'data-ve-marquee' )
+	->not->toContain( '/vendor/visual-editor-renderer-blade/frontend/interactivity.js' )
 	->not->toContain( 'data-ve-accordion' )
 	->not->toContain( 'data-ve-tabs' );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/visual-editor-renderer-blade/tests/Feature/BlocksStylesComponentTest.php`
around lines 24 - 42, Add a negative assertion to the existing tests that render
Blade::render('<x-ve-blocks-styles :interactive="false" />') (e.g., the "emits
the grid + marquee..." and/or "emits the breadcrumbs..." examples) to assert
that the interactivity script is not emitted; specifically, add
expect($rendered)->not->toContain(...) for the interactivity asset path
(interactivity.js) and/or the interactivity marker (e.g.,
'data-ve-interactivity') so the test verifies the script is omitted when
:interactive="false".
packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/author-social-icons.blade.php (1)

6-120: 🏗️ Heavy lift

Extract shared social-chip preparation into a single helper to avoid divergence.

This block duplicates almost the entire normalize/filter/sanitize/style pipeline that also exists in packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/social-share-content.blade.php. Keeping this logic in one place will prevent drift in validation/sanitization behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/author-social-icons.blade.php`
around lines 6 - 120, The template duplicates the
normalize/filter/sanitize/style pipeline (the code that builds
$selected/$selectedSlugs, iterates $linksRaw, calls SocialIconRegistry::author
and UrlSanitizer::safe, builds $chips, $styleParts, $styleAttr, $showIcon and
$showLabel); extract that logic into a single reusable helper (e.g., a function
prepare_author_social_icons(array $attributes): array or a class method) that
accepts the block $attributes and returns an array with keys chips, styleAttr,
showIcon, showLabel (and any other derived values like radius if needed);
implement the helper somewhere reusable (a view helper or service class), move
the pipeline code into it (preserving all validation/sanitization rules and
variable names like SocialIconRegistry::author and UrlSanitizer::safe), and
replace the duplicated blocks in each template with a call to the new helper and
assignment from its return value.
resources/js/visual-editor/blocks/_shared/social-icons.ts (1)

60-75: ⚡ Quick win

Avoid positional coupling in SHARE_PLATFORMS.

Using AUTHOR_PLATFORMS[0/1/2/5] makes this brittle; reordering AUTHOR_PLATFORMS can silently map the wrong icons. Prefer slug-based lookup.

Refactor sketch
+function requireAuthorPlatform(slug: string): SocialIconDefinition {
+    const platform = AUTHOR_PLATFORMS.find((entry) => entry.slug === slug);
+    if (!platform) {
+        throw new Error(`Unknown author platform slug: ${slug}`);
+    }
+    return platform;
+}
+
 const SHARE_PLATFORMS: ReadonlyArray<SocialIconDefinition> = [
-    AUTHOR_PLATFORMS[0], // facebook
-    AUTHOR_PLATFORMS[1], // twitter / X
-    AUTHOR_PLATFORMS[2], // mastodon
+    requireAuthorPlatform('facebook'),
+    requireAuthorPlatform('twitter'),
+    requireAuthorPlatform('mastodon'),
     {
       slug: 'reddit',
       // ...
     },
     {
       slug: 'pinterest',
       // ...
     },
-    AUTHOR_PLATFORMS[5], // email
+    requireAuthorPlatform('email'),
 ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/visual-editor/blocks/_shared/social-icons.ts` around lines 60 -
75, SHARE_PLATFORMS is brittle because it references AUTHOR_PLATFORMS by numeric
indices; change it to build the array by slug lookup (e.g.,
findPlatform('facebook'), findPlatform('twitter'), findPlatform('mastodon'),
findPlatform('email')) instead of AUTHOR_PLATFORMS[0/1/2/5]; implement a small
helper (e.g., getAuthorPlatformBySlug(slug): SocialIconDefinition | undefined)
that returns the matching entry from AUTHOR_PLATFORMS and use it when
constructing SHARE_PLATFORMS, providing the inline literal definitions (reddit,
pinterest) unchanged and falling back to a safe default or excluding missing
entries if find returns undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/visual-editor.php`:
- Around line 334-337: The breadcrumbs resolver is added under the 'breadcrumbs'
config key but the inserter allow-list 'enabled_blocks' still omits the block;
open the same config file and add 'artisanpack/breadcrumbs' to the
enabled_blocks array (the allow-list that controls which blocks are insertable)
so the resolver becomes insertable via the editor.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/marquee.blade.php`:
- Line 32: The template currently emits raw HTML for the marquee with {!!
$content !!}, which can cause stored XSS if the marquee content is
user-controlled; change the rendering to either escape the content (use Blade's
escaped echo for $content) or run it through a trusted sanitizer/HTML whitelist
function before echoing, ensuring you still allow safe markup if needed; update
the ap-marquee__text output to use the sanitized/escaped value (replace raw {!!
$content !!} with the escaped/sanitized variant) so $content/marqueeContent is
never emitted unsanitized.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-field.blade.php`:
- Around line 29-32: Both templates currently build form-control IDs only from
json_encoded attributes which can duplicate across instances; update each site
to append a per-instance identifier from the render context to the computed ID
(keep the existing md5(attributes) hash as a secondary stabilizer). In
packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-field.blade.php
(lines 29-32) change the $inputId construction to include a per-instance token
(for example append the block's runtime/client id available in the render
context such as $attributes['id'] or a $clientId/$blockId or uniqid()), e.g.
combine substr(md5(json_encode($attributes)),0,8) with that token; in
packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-filters-taxonomy.blade.php
(lines 90-94) make the same change for $selectId so each block instance yields a
unique ID while preserving the existing hash as a secondary stabilizer.

In `@packages/visual-editor-renderer-blade/src/BlockRenderer.php`:
- Around line 109-116: The dynamic rendering path drops raw child blocks when
falling back to static, so update the call and implementations to propagate
innerBlocks: change the call in BlockRenderer (currently renderDynamic($name,
$attributes, $innerBlocksHtml)) to pass the normalized $innerBlocks as a fourth
argument (renderDynamic($name, $attributes, $innerBlocksHtml, $innerBlocks)),
then update the renderDynamic method signature and its internal fallback to call
renderStatic($name, $attributes, $innerBlocksHtml, $innerBlocks) (instead of
passing an empty array) so any fallback static rendering receives the full
child-block tree.

In `@resources/js/visual-editor/blocks/tab-section/save.tsx`:
- Around line 7-9: Both save entrypoints currently return null which prevents
nested blocks from serializing; update the TabSectionSave
(resources/js/visual-editor/blocks/tab-section/save.tsx lines 7-9) to render the
section's saved inner blocks (use InnerBlocks.Content) instead of null, and
likewise update the TabsSave (resources/js/visual-editor/blocks/tabs/save.tsx
lines 7-9) to render the parent's saved inner blocks (use InnerBlocks.Content);
ensure the appropriate InnerBlocks import (from `@wordpress/block-editor` or
wp.blockEditor) is present at the top of each file so the rendered content is
serialized.

In `@resources/js/visual-editor/editor/content-type-registry.ts`:
- Around line 63-67: parseFromElement currently accepts any non-empty plural and
caches it, which allows malformed values to later break
SingleContentEdit.resolveCollection when used in `${API_BASE}/${collection}`;
update parseFromElement (the code handling rawPlural -> plural) to validate and
normalize plurals before caching by trimming, lowercasing, and only accepting a
URL-safe path segment (e.g. match /^[a-z0-9-_]+$/i); if the rawPlural fails
validation, fall back to the safe default `${slug}s`; this ensures the cached
plural (variable plural) is always a safe path segment so resolveCollection and
API calls won’t produce broken URLs.

In `@src/Resources/PostResolver.php`:
- Around line 159-167: The adjacent-post container currently calls
adjacentPost() which relies on queryAdjacentPost() fallback that only compares
published_at and can return a different post type or the same row on ties;
update queryAdjacentPost() (used by adjacentPost()) to restrict fallback queries
to the same post type (e.g., WHERE post_type = $post->post_type) and to exclude
the current post id (WHERE ID != $post->ID), and add a deterministic tiebreaker
in the ordering (e.g., ORDER BY published_at ASC/DESC, ID ASC/DESC) so equal
timestamps return the true neighbor rather than a different type or the same
row. Ensure any call sites that expect adjacentPost() behavior need no change.
- Around line 866-979: The authorSocialUrl function currently returns raw
author-provided URLs and can allow dangerous schemes; update authorSocialUrl to
validate and allowlist schemes before returning. After computing $value (the
chosen candidate), trim it, then if it starts with '/' or '#' treat it as a safe
relative URL and return it; otherwise parse the scheme via parse_url($value,
PHP_URL_SCHEME) (lowercased) and only allow a small whitelist such as 'http',
'https', 'mailto', and 'tel'; for 'email' keep the existing mailto:
normalization but only after validation; if the scheme is missing but the value
looks like an email address allow mailto:, and if the scheme is not in the
allowlist return an empty string. Apply these checks in authorSocialUrl to
ensure only safe link targets are stamped.

In `@src/Resources/QueryInliner.php`:
- Around line 339-350: The related-posts query currently hardcodes 'category' in
the $queryAttrs (see where $queryAttrs is built) causing tag-only relatedness to
fail; change the logic to determine taxonomy and term IDs from the host post by
adding a helper like hostRelatedTerms() that returns [taxonomy, termIds] (using
hostTermIds() or extractTermIds($post->categories/$post->tags) as shown in the
proposed snippet), then replace the hardcoded 'taxonomy' => 'category' and
'terms' => $termIds with 'taxonomy' => $taxonomy and 'terms' => $termIds (where
$taxonomy and $termIds come from hostRelatedTerms($host)); apply the same
replacement at the second site where a similar $queryAttrs block is built (the
block around lines 442-460) so both related-post queries use the correct
taxonomy.

---

Minor comments:
In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/author-social-icons.blade.php`:
- Line 124: Replace the hardcoded concatenation used for the aria-label on the
anchor with a translatable sprintf-style string: change the aria-label
expression on the ap-author-social-icons__chip anchor to use sprintf( __(
'Author on %s' ), $chip['label'] ) (inside Blade echoing) so the label becomes
translatable via the %s placeholder while preserving the existing $chip['label']
and $styleAttr.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-filters.blade.php`:
- Around line 14-16: In search-filters.blade.php (lines 14-16) the sanitized
$postType can become empty; after the preg_replace call, check if $postType ===
'' and if so set it back to 'post' before rendering the hidden input. In
search-filters-taxonomy.blade.php (lines 19-21) do the same for $taxonomy: after
sanitization check if $taxonomy === '' and if so reset it to 'category' before
using it for request lookups and as the <select name> value; no other changes
are required at the sibling site.

In `@packages/visual-editor-renderer-blade/src/Resolvers/BreadcrumbsResolver.php`:
- Around line 232-236: The 'current' normalization uses "! empty(
$entry['current'] )" which treats strings like "false" as truthy; update the
BreadcrumbsResolver normalized array assignment so the 'current' value is
strictly coerced to boolean (e.g. use filter_var($entry['current'],
FILTER_VALIDATE_BOOLEAN) or an explicit boolean check) instead of !empty(),
replacing the existing 'current' => ! empty( $entry['current'] ) expression in
the block that builds $normalized[].

In
`@packages/visual-editor-renderer-react/src/blocks/artisanpack/breadcrumbs.tsx`:
- Line 2: Remove the internal phase label "CW0 pilot" from the header comment in
the artisanpack/breadcrumbs renderer; edit the header comment that currently
reads "React renderer for the `artisanpack/breadcrumbs` block (CW0 pilot —
`#496`)" to a neutral form such as "React renderer for the
`artisanpack/breadcrumbs` block" so no internal phase labels remain; ensure no
other occurrences of "CW0", "CW1", etc. remain in that header comment or nearby
comments.

In
`@packages/visual-editor-renderer-react/src/blocks/artisanpack/searchCluster.tsx`:
- Around line 67-69: The IDs computed from shortHash (e.g., the inputId constant
created with shortHash(`${label}|${placeholder}|${searchValue}`)) are only
value-derived and can collide across repeated block instances; include a
per-instance unique suffix (for example a renderer/instance identifier passed as
a prop or obtained from context like instanceId or rendererInstanceId) when
constructing these IDs so they are unique per block instance. Update the inputId
expression to append or incorporate that instance identifier (use the same
instance identifier consistently wherever related IDs are computed), and make
the same change to the other ID(s) derived from values later in the file (the ID
at the 145–146 site mentioned in the review) so label/aria relationships remain
correct across multiple instances. Ensure the instance identifier is
required/validated or generated once per block instance and reused for all IDs
in this component.

In `@packages/visual-editor-renderer-vue/src/blocks/artisanpack/breadcrumbs.ts`:
- Line 2: The header comment for the Vue renderer of the
`artisanpack/breadcrumbs` block contains an internal phase label ("CW0 pilot —
`#496`"); remove that phrase so the comment reads simply "Vue renderer for the
`artisanpack/breadcrumbs` block." Locate the top-of-file header comment in the
breadcrumbs renderer and delete the internal phase-label wording, leaving the
rest of the comment intact.

In `@packages/visual-editor-renderer-vue/src/blocks/artisanpack/searchCluster.ts`:
- Around line 72-74: The generated control IDs (e.g., inputId produced via
shortHash(`${label}|${placeholder}|${searchValue}`)) only use attribute content
so identical blocks collide; include a per-instance unique seed when composing
these IDs (for example `${instanceSeed}|${label}|${placeholder}|${searchValue}`)
and persist that seed across renders (use a stable node/block id if available or
generate-and-store one on mount/useRef) so IDs remain unique and stable; apply
this change to the inputId construction shown and the analogous ID generation at
the other site (the occurrence referenced at lines 178-179) so both places use
the same per-instance seed.

In `@resources/js/visual-editor/blocks/breadcrumbs/edit.tsx`:
- Around line 73-76: The SelectControl options use __() with a computed value
(option.label) so i18n extraction will miss strings; update the options
construction so the first argument to __() is a static literal for each label
instead of __((option.label), TEXT_DOMAIN). Concretely, replace the current
SEPARATOR_OPTIONS.map(...) usage in the options prop with either (a) prebuild
SEPARATOR_OPTIONS so each entry's label is already the result of a literal call
like __('Slash', TEXT_DOMAIN) and then use that label directly, or (b) map using
a switch/lookup that passes a string literal into __() for each known option
(e.g., __('Slash', TEXT_DOMAIN), __('Arrow', TEXT_DOMAIN), etc.), keeping value
as option.value; ensure the code references SEPARATOR_OPTIONS and TEXT_DOMAIN so
the correct symbols are updated.

In `@resources/js/visual-editor/blocks/breadcrumbs/index.ts`:
- Around line 5-6: Remove the internal rollout/provenance wording from the top
docblock that mentions `@wordpress/blocks.registerBlockType`—edit the comment to
strip any internal phase labels (e.g., "CW0 pilot", "provenance", or similar
rollout/legacy tags) so only neutral, public-facing documentation remains; leave
the factual description of the block and its behavior but remove internal
provenance phrases.

In `@resources/js/visual-editor/blocks/search-filters/edit.tsx`:
- Around line 62-69: The options array built from getContentTypes() can lack a
'post' entry while the UI still uses value='post', causing an unmatched blank
selection; update the logic that creates options (the options variable) to
ensure a selectable default by checking if options.some(o => o.value === 'post')
and if false add { value: 'post', label: __('Posts', TEXT_DOMAIN) } (use push or
unshift as desired) so 'post' is present whenever missing.

In `@resources/js/visual-editor/blocks/single-content/variations.ts`:
- Around line 23-24: Localize the inserter-visible strings in variations.ts by
replacing the hardcoded title and description with i18n calls: use sprintf( __(
'Single %s', 'your-text-domain' ), type.label ) for title and sprintf( __(
'Render a single %s and scope the inner blocks to it.', 'your-text-domain' ),
type.label.toLowerCase() ) for description (or use __ with concatenation if
preferred), and ensure you import/wp.i18n functions (__, sprintf) at the top of
the file; update the title and description properties accordingly so they are
translatable.

In `@resources/js/visual-editor/blocks/skills-slider/edit.tsx`:
- Around line 131-135: The ARIA range advertised by the progressbar is
inconsistent: aria-valuemin is set to 0 while the control clamps skillLevel to
1–100 (safeLevel) and the inspector only allows 1–100; update the aria-valuemin
attribute to match the actual minimum (set aria-valuemin={1} or use the existing
min constant/prop if one exists) in the element that renders role="progressbar"
(the same JSX that sets aria-valuenow={safeLevel} and aria-label={ariaLabel ||
__('Skill level', TEXT_DOMAIN)}), so the announced range aligns with the real
control bounds.

In `@src/View/Components/VisualEditorComponent.php`:
- Around line 66-69: The PHPDoc for the property $contentTypes and the PHPDoc
for the return shape of resolveContentTypes() are out of sync with actual data
(they omit the "plural" key); update both PHPDoc annotations to use array<int,
array{slug: string, plural: string, label: string}> so the declared shape
matches the array returned by resolveContentTypes().

---

Nitpick comments:
In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/author-social-icons.blade.php`:
- Around line 6-120: The template duplicates the normalize/filter/sanitize/style
pipeline (the code that builds $selected/$selectedSlugs, iterates $linksRaw,
calls SocialIconRegistry::author and UrlSanitizer::safe, builds $chips,
$styleParts, $styleAttr, $showIcon and $showLabel); extract that logic into a
single reusable helper (e.g., a function prepare_author_social_icons(array
$attributes): array or a class method) that accepts the block $attributes and
returns an array with keys chips, styleAttr, showIcon, showLabel (and any other
derived values like radius if needed); implement the helper somewhere reusable
(a view helper or service class), move the pipeline code into it (preserving all
validation/sanitization rules and variable names like SocialIconRegistry::author
and UrlSanitizer::safe), and replace the duplicated blocks in each template with
a call to the new helper and assignment from its return value.

In
`@packages/visual-editor-renderer-blade/tests/Feature/BlocksStylesComponentTest.php`:
- Around line 24-42: Add a negative assertion to the existing tests that render
Blade::render('<x-ve-blocks-styles :interactive="false" />') (e.g., the "emits
the grid + marquee..." and/or "emits the breadcrumbs..." examples) to assert
that the interactivity script is not emitted; specifically, add
expect($rendered)->not->toContain(...) for the interactivity asset path
(interactivity.js) and/or the interactivity marker (e.g.,
'data-ve-interactivity') so the test verifies the script is omitted when
:interactive="false".

In `@packages/visual-editor-renderer-vue/tests/BlockTree.test.ts`:
- Around line 576-577: Update the test title string in the it(...) call that
currently reads "carries the `#338` a11y fix forward to artisanpack/search (I4
fork)" by removing the internal phase label "(I4 fork)" and replacing it with a
neutral description (e.g., "carries the `#338` a11y fix forward to
artisanpack/search"); leave the test body and makeBlock(...) invocation
unchanged.

In `@resources/js/visual-editor/blocks/_shared/social-icons.ts`:
- Around line 60-75: SHARE_PLATFORMS is brittle because it references
AUTHOR_PLATFORMS by numeric indices; change it to build the array by slug lookup
(e.g., findPlatform('facebook'), findPlatform('twitter'),
findPlatform('mastodon'), findPlatform('email')) instead of
AUTHOR_PLATFORMS[0/1/2/5]; implement a small helper (e.g.,
getAuthorPlatformBySlug(slug): SocialIconDefinition | undefined) that returns
the matching entry from AUTHOR_PLATFORMS and use it when constructing
SHARE_PLATFORMS, providing the inline literal definitions (reddit, pinterest)
unchanged and falling back to a safe default or excluding missing entries if
find returns undefined.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

Comment thread config/visual-editor.php
$textStyle = 'animation: ap-marquee-scroll ' . $speed . 's linear infinite;';
@endphp
<div{!! $classAttr . $styleAttr . $idAttr !!}>
<p class="ap-marquee__text" style="{{ $textStyle }}">{!! $content !!}</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape or sanitize marquee content before rendering.

Line 32 outputs $content with raw Blade echo ({!! !!}), which can introduce stored XSS if marqueeContent is user-controlled at any point in the pipeline.

🔧 Proposed fix
-	<p class="ap-marquee__text" style="{{ $textStyle }}">{!! $content !!}</p>
+	<p class="ap-marquee__text" style="{{ $textStyle }}">{{ $content }}</p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p class="ap-marquee__text" style="{{ $textStyle }}">{!! $content !!}</p>
<p class="ap-marquee__text" style="{{ $textStyle }}">{{ $content }}</p>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/marquee.blade.php`
at line 32, The template currently emits raw HTML for the marquee with {!!
$content !!}, which can cause stored XSS if the marquee content is
user-controlled; change the rendering to either escape the content (use Blade's
escaped echo for $content) or run it through a trusted sanitizer/HTML whitelist
function before echoing, ensuring you still allow safe markup if needed; update
the ap-marquee__text output to use the sanitized/escaped value (replace raw {!!
$content !!} with the escaped/sanitized variant) so $content/marqueeContent is
never emitted unsanitized.

Comment thread packages/visual-editor-renderer-blade/src/BlockRenderer.php Outdated
Comment on lines +7 to +9
export default function TabSectionSave(): null {
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist the inner-block tree in both save entrypoints. Both save components currently return null, so the nested tab sections never serialize and the block tree cannot round-trip.

  • resources/js/visual-editor/blocks/tab-section/save.tsx#L7-L9: emit the section’s saved inner blocks here.
  • resources/js/visual-editor/blocks/tabs/save.tsx#L7-L9: emit the parent’s saved inner blocks here.
📍 Affects 2 files
  • resources/js/visual-editor/blocks/tab-section/save.tsx#L7-L9 (this comment)
  • resources/js/visual-editor/blocks/tabs/save.tsx#L7-L9
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/visual-editor/blocks/tab-section/save.tsx` around lines 7 - 9,
Both save entrypoints currently return null which prevents nested blocks from
serializing; update the TabSectionSave
(resources/js/visual-editor/blocks/tab-section/save.tsx lines 7-9) to render the
section's saved inner blocks (use InnerBlocks.Content) instead of null, and
likewise update the TabsSave (resources/js/visual-editor/blocks/tabs/save.tsx
lines 7-9) to render the parent's saved inner blocks (use InnerBlocks.Content);
ensure the appropriate InnerBlocks import (from `@wordpress/block-editor` or
wp.blockEditor) is present at the top of each file so the rendered content is
serialized.

Comment thread resources/js/visual-editor/editor/content-type-registry.ts Outdated
Comment thread src/Resources/PostResolver.php
Comment thread src/Resources/PostResolver.php
Comment thread src/Resources/QueryInliner.php Outdated
CR's auto-review on #577 surfaced nine inline findings against the
crosswinds-port cumulative diff. Seven are addressed below; the
remaining two are documented at the bottom with the reason for
skipping.

config/visual-editor.php — enabled_blocks
  CR flagged `artisanpack/breadcrumbs` as configured (resolver section
  present) but missing from the inserter allow-list. The same gap
  applied to every other CW0–CW7 block — none of the 25 first-party
  ports had been added. Listing them all in cluster order so the
  inserter surfaces them and the EnabledBlockNamesTest V1 snapshot
  stays the source of truth.

packages/visual-editor-renderer-blade/src/BlockRenderer.php
  Propagate the normalized `innerBlocks` tree through both fallback
  paths in `renderDynamic` — missing dynamic handler and a thrown
  exception — so static partials downstream still see the full child
  tree.

  Also adds a per-render `$renderIndex` counter (resets in `render()`,
  increments in `renderBlock`, threaded into `renderStatic` and the
  partial's `$data`) so templates that need a per-instance suffix can
  stay unique even when sibling blocks carry identical attributes.

packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/{search-field,search-filters-taxonomy}.blade.php
  Mix `$renderIndex` into the form-control `id`. The attribute-hash
  suffix stays as a secondary stabilizer so the id is still readable
  in the single-instance case, but two identical-attribute siblings
  no longer collide. The label `for=` binding can no longer target
  the wrong control.

resources/js/visual-editor/editor/content-type-registry.ts
  Constrain `plural` (later interpolated into `${API_BASE}/${plural}`
  by `SingleContentEdit.resolveCollection()`) to the same
  `[a-z0-9_-]` shape as the slug, falling back to `${slug}s` for
  anything malformed. A registry entry with `plural: "../admin"` or
  similar can no longer produce a broken / traversal-prone REST URL.

src/Resources/PostResolver.php — queryAdjacentPost
  The Eloquent fallback now scopes to the host's `post_type`, excludes
  the current post id, and adds an id-based tiebreaker on the same
  `published_at`. Adjacent containers can no longer surface a
  different post type as the "next post" or — on equal timestamps —
  return the host post itself.

src/Resources/PostResolver.php — authorSocialUrl
  Validate the email candidate via `FILTER_VALIDATE_EMAIL` before
  the `mailto:` normalization and reject anything outside an
  `http`/`https`/`mailto`/`tel` scheme allow-list. Author profile
  fields can be host-editable; a `javascript:` value there would have
  rendered as the `href` of a chip on the public frontend.

src/Resources/QueryInliner.php — related-posts taxonomy detection
  Add `hostRelatedTerms()` that returns `[taxonomy, termIds]` by
  preferring `$post->categories`, falling back to `$post->tags`
  (`post_tag`), and finally degrading to the legacy
  `hostTermIds()` scoped to `category` for hosts that only model a
  generic `terms` collection. The hard-coded `'taxonomy' => 'category'`
  in `expandRelatedPosts()` is replaced with the resolved taxonomy
  so tag-only posts now get a related-posts query that exercises the
  right index.

Skipped (documented):

marquee.blade.php — `{!! $content !!}` XSS finding.
  The marquee template follows the same `source: 'html'` /
  `{!! $content !!}` pattern as 20+ other rich-text blocks
  (paragraph, heading, quote, pullquote, callout, list, …). The
  underlying concern is real but the right fix is a package-wide
  HTML sanitizer at the rendering boundary across all three
  renderers — not a one-off escape on marquee that would lose the
  formatted-text round-trip and silently diverge from the rest of
  the rich-text pipeline. Filing as its own follow-up.

tabs/tab-section save returning `null`.
  CR read this as a round-trip bug. It isn't: tabs/tab-section
  follow the same dynamic-container pattern as accordions /
  accordion / accordion-title / accordion-body / grid / grid-item.
  Gutenberg's serializer preserves the inner-block tree as nested
  comment delimiters regardless of what `save` returns; the
  renderers walk the parsed tree, not the saved HTML, so the
  round-trip is intact.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/visual-editor.php`:
- Around line 203-205: Replace the legacy-source and phase-label wording in the
allow-list comments with neutral phrasing that does not mention CW phase labels
or crosswinds terminology; specifically, in config/visual-editor.php (lines
203-205) update the comment to neutral language such as referencing the upstream
change/issue only (e.g., "First-party blocks ported from upstream (see `#495`);
child blocks are also listed so the inserter allow-list does not filter them out
of their parent's template."), and in
tests/Unit/VisualEditor/EnabledBlockNamesTest.php (lines 87-90) make the same
replacement so the test docs use the identical neutral phrasing referencing only
the upstream/issue number.

In `@resources/js/visual-editor/editor/content-type-registry.ts`:
- Around line 68-81: The fallback plural currently trusts the raw slug (`slug`)
which is only trimmed; when `rawPlural` is invalid you must produce a safe
plural using the same constraints as `SAFE_PLURAL_PATTERN` instead of
`${slug}s`. Change the fallback to first normalize/sanitize the slug (e.g.
lower-case, trim, and replace or strip characters that don't match
SAFE_PLURAL_PATTERN) into a new `normalizedSlug`, then set `plural` to
`${normalizedSlug}s` only if `SAFE_PLURAL_PATTERN.test(normalizedSlug)`; if that
still fails, use a safe hardcoded fallback (e.g. "items") so `plural` is
guaranteed to match the safe pattern. Ensure you update the code paths that
compute `normalizedPlural`/`plural` so the `slug`-based fallback runs the same
validation/sanitization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ed35a0f0-14f5-46ae-a387-7af10b281a3b

📥 Commits

Reviewing files that changed from the base of the PR and between 34eba9a and c9182c1.

📒 Files selected for processing (8)
  • config/visual-editor.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-field.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-filters-taxonomy.blade.php
  • packages/visual-editor-renderer-blade/src/BlockRenderer.php
  • resources/js/visual-editor/editor/content-type-registry.ts
  • src/Resources/PostResolver.php
  • src/Resources/QueryInliner.php
  • tests/Unit/VisualEditor/EnabledBlockNamesTest.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/Resources/PostResolver.php
  • src/Resources/QueryInliner.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/search-filters-taxonomy.blade.php

Comment thread config/visual-editor.php Outdated
Comment thread resources/js/visual-editor/editor/content-type-registry.ts Outdated
config/visual-editor.php + tests/Unit/VisualEditor/EnabledBlockNamesTest.php
  Rewrite the two allow-list comments I added in the previous fix
  commit to drop the legacy-source name and phase labels. The
  repo's convention (see CLAUDE.md) is that only the issue number
  is acceptable; the rest is noise that leaks rebrand work into the
  in-code commentary.

resources/js/visual-editor/editor/content-type-registry.ts
  Tighten the plural fallback. The previous round constrained
  `plural` against `SAFE_PLURAL_PATTERN` but the fallback path —
  `${slug}s` — still trusted an unvalidated slug, so a registry
  entry like `{slug: "../admin", label: "X"}` would slip through
  and produce a malformed REST collection segment. Lowercase the
  slug, run it through the same pattern, and drop the entry
  entirely if it fails. The `${slug}s` fallback is now guaranteed
  safe because the slug has been validated by the time we get
  there.
@ViewFromTheBox
ViewFromTheBox merged commit 8bef597 into release/1.1 Jun 13, 2026
6 checks passed
@github-project-automation github-project-automation Bot moved this from In Review to Done in ArtisanPack UI Overview Jun 13, 2026
@ViewFromTheBox
ViewFromTheBox deleted the feature/495-port-crosswinds-blocks branch June 13, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant