Skip to content

Pattern Builder 2.0: theme pattern files become the single source of truth - #46

Merged
pbking merged 9 commits into
mainfrom
claude/pattern-builder-2-upgrade-t523cj
Aug 29, 2026
Merged

Pattern Builder 2.0: theme pattern files become the single source of truth#46
pbking merged 9 commits into
mainfrom
claude/pattern-builder-2-upgrade-t523cj

Conversation

@pbking

@pbking pbking commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Pattern Builder 2.0

This is the 2.0 upgrade following the architecture Synced Patterns for Themes 2.0 proved out: no custom post type hiding the patterns, no REST interception — theme pattern files are the single source of truth, and every 1.x feature is rebuilt on mechanisms WordPress core already uses.

Companion PR: Twenty-Bellows/synced-patterns-for-themes#6 — the companion now stands down entirely when this plugin is active.

The architecture

Theme patterns are file-backed REST entities — the wp_template model. A rowless pb_pattern post type (zero DB rows, ever) hangs Pattern_Builder_REST_Patterns_Controller off core routing at /pattern-builder/v1/patterns, with string IDs (theme-slug/pattern-name). Reads come from the pattern files (child and parent theme); writes go back to the files. Because the type is show_in_rest, the block editor auto-creates a matching client-side entity from /wp/v2/types — undo, dirty tracking, and the save flow all come from core's entity layer, with zero masquerade.

Synced patterns ride the core/pattern content runtime vendored from Synced Patterns for Themes 2.0: core/pattern gets a content attribute + pattern/overrides context (the exact shape core/block has), a render callback attaches the pattern's blocks as inner blocks, and core's own core/pattern-overrides binding source resolves overrides. An inserted synced theme pattern is a plain pattern block:

<!-- wp:pattern {"slug":"my-theme/hero","content":{...}} /-->

No post ID anywhere, and per-instance overrides stay isolated. Synced user patterns stay core-native: they insert as core/block references.

Coexistence is one check in one place. Pattern Builder always registers the full stack — the vendored runtime (kept logic-identical to the companion's) plus the editing layer. When both plugins are installed, the companion sees PATTERN_BUILDER_VERSION at plugins_loaded and stays entirely unloaded; deactivate Pattern Builder and the companion takes over with identical rendering. Pattern Builder also clears the companion's per-theme transient after file writes, so the companion never wakes to a stale cache.

Editing surfaces — always the WordPress editor, never a custom one:

  • Post editor: in-context — the sidebar's Edit button swaps the pb_pattern (or wp_block) entity into the canvas without leaving the post; the entity is pre-resolved first to avoid an empty-content race.
  • Site Editor: user patterns are edited in place via core's /wp_block route. Theme patterns cannot bind to the Site Editor canvas — core hard-codes the entity types its canvas resolves and keeps route registration private — so Edit opens core's own edit-post editor bound to the pattern file entity, and its Back button returns to the exact Site Editor screen you left (server-validated back URL).
  • Appearance → Pattern Builder is a Site-Editor-style library: a category rail with counts, a grid of uniform 1:1 pattern cards, and a details sidebar for the selected pattern carrying the same panels the editor shows, with Save and Edit actions. Clicking Edit on a user pattern opens the Site Editor's pattern canvas; a theme pattern boots core's edit-post editor (wp.editPost.initializeEditor) against the pb_pattern entity — the genuine post-editor chrome saving straight to the file. Deep-linkable via &pattern={id}; a history.replaceState guard keeps the page's URL, and the app pins its bottom edge to the viewport so the panes scroll internally.

Feature work

  • All pattern metadata round-trips through the file header: title, description, categories, keywords, block types, post types, template types, viewport width (1.x read it and lost it on save), inserter visibility, synced status. New Metadata panel; Bindings panel re-enabled as the slot-authoring UI.
  • Synced status is editable both ways for user patterns too — core registers the wp_pattern_sync_status meta with enum [partial, unsynced], so "synced" is the absence of the meta; the panel writes null (the REST meta delete) instead of an enum-rejected empty string.
  • Edit Pattern in the block toolbar: selecting a synced theme pattern instance (core/pattern) on the canvas offers Edit Pattern next to the runtime's Reset/Detach — in-context in the post editor, the Pattern Builder editor elsewhere. Shown only when the slug resolves to a file-backed pattern. (Synced user instances are core/block, where core provides Edit original.)
  • Conversions both ways as real REST semantics: POST /patterns {fromWpBlock: N} converts user→theme (images imported as theme assets, post deleted); PUT /patterns/{id} {source:"user"} converts theme→user (images exported to the media library, file deleted).
  • Migration runs once on upgrade: wp:block {"ref":N} references to 1.x mirror posts are rewritten to wp:pattern slugs in post content and theme files while the rows still exist as the ID→slug map, then the mirror posts and 1.x capabilities are removed. uninstall.php added (1.x had none).
  • WordPress.org shipping workflow (same as the companion's): npm run plugin-ship / :dry-run / :reset — preflight version agreement, .distignore-defined ship set, wp.org assets in .wordpress-org/, SVN trunk + tag + assets committed in one go. Rehearsed end-to-end against a local SVN repository.
  • Theme patterns now appear in the core inserter per their own Inserter: header (1.x hid them all and re-listed them as fake wp_blocks).
  • The REST response advertises wp:action-publish, so the editor's save button reads Save/Update instead of degrading to "Submit for Review".

Removed / fixed

  • The tbell_pattern_block CPT, its capability family, and the per-request DB upserts (TWE-369 — the performance issue is gone by construction).
  • All four /wp/v2/blocks interception filters — including the unauthenticated file deletion and unauthenticated edit-context read paths that rode rest_pre_dispatch ahead of permission callbacks. Every route now has a permission callback (edit_posts read / edit_theme_options write), and a test pins that no rest_pre_dispatch filter remains.
  • Pattern_Builder_Security was declared in the global namespace but called from namespaced code — a latent fatal on every file write. Fixed.
  • TWE-370 (create-time "Theme" choice was cosmetic), TWE-371 (-x-x- slug encoding), TWE-373 (parent theme ignored), TWE-374 (sidebar not refreshing after create), the broken drag payload, the user-pattern list truncated at 10, and the fixture theme's template referencing patterns from the wrong namespace.
  • Requires WordPress 6.8 / PHP 7.4 (the render layer needs WP_Block::refresh_context_dependents(); 1.x already called a 6.8-only function despite its 6.6 header).

Testing

  • PHP: 91 tests / 276 assertions in wp-env — the rewritten REST suite (list/read/write/metadata round-trip/conversions/delete/auth/action links), the migration suite, the vendored runtime suites (render, resolver, editor REST, synced lookup), and the 28 localization tests.
  • JS: 14 tests (override transforms + utils), lint:js, lint:css, and phpcs all clean.
  • Real browser (Playwright/Chromium against wp-env, WP trunk 7.2-alpha):
    • Pattern Builder page: category rail counts/filtering, uniform square cards, details sidebar Save writing the theme file, Edit opening the real edit-post editor → type → Save → the theme .php file on disk updated with an intact header; page reload survives; Back returns to the origin screen; no page scrolling.
    • Site Editor round-trip: sidebar Edit on a theme pattern → core editor with back URL → Back lands on the exact Site Editor screen. User patterns edited in place in the Site Editor (/wp_block route), from within it and deep-linked from the grid.
    • Post editor: sidebar insert of a synced theme pattern lands as core/pattern (not a copy) and renders on the front end through the runtime; a synced user pattern lands as core/block {ref} and renders; in-context pb_pattern entity editing; Edit Pattern toolbar button navigates in-context.
    • Sync toggle: an unsynced user pattern flips to synced (meta deleted server-side) and the collection endpoint reports it synced.
    • Override editing: typing in an instance slot stores on the host block's content attribute; a second instance of the same pattern stays isolated.
    • Coexistence, both directions: with both plugins active, the companion's classes are not loaded and this plugin's Pattern_Block owns the core/pattern render callback; deactivating Pattern Builder hands rendering back to the companion identically.

Notes for reviewers

  • includes/class-pattern-block.php, class-pattern-resolver.php, class-block-markup.php, class-inner-html-processor.php, class-synced-patterns.php, class-editor-support.php and src/runtime/* are vendored from synced-patterns-for-themes (namespace/identifier renames only) and should be kept logic-identical to it — that release-time invariant is what makes the coexistence hand-off invisible.
  • docs/architecture.md is marked historical; the new design is in docs/architecture-2.0.md.
  • Rowless-entity trade-off, documented: no revisions for theme patterns (files + version control are the history), and no autosaves (lockPostAutosaving is dispatched; the editor's one autosave probe 404s harmlessly).
  • If core ever opens Site Editor route registration to plugins, theme patterns can move fully into the Site Editor canvas — the only thing that changes is the URL the Edit button targets.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB

…truth

Removes the 1.x DB-mirror + REST-hijacking architecture entirely and
rebuilds pattern management on mechanisms core already uses:

- Theme patterns are file-backed REST entities: a rowless pb_pattern post
  type (the wp_template model — zero DB rows) hangs a string-ID controller
  off core routing at /pattern-builder/v1/patterns. Reads come from the
  pattern files (child + parent theme); writes go back to the files. The
  block editor auto-creates a matching client entity, so theme patterns get
  entity-powered editing (undo, dirty tracking, save flow) for free.
- Synced patterns work through the core/pattern content attribute — the
  runtime vendored from Synced Patterns for Themes 2.0. Inserted copies are
  plain wp:pattern references with per-instance overrides; no post ID
  anywhere. When the companion plugin is active it owns the runtime and
  Pattern Builder defers (plugins_loaded class_exists gate, plus an
  attribute-presence guard in the filter itself).
- New editing surfaces: a full-screen editor on Appearance → Pattern
  Builder (public EditorProvider + BlockCanvas bound to the entity, with
  browse/search/create) and in-context entity editing in the post editor.
- Full metadata management: title, description, categories, keywords,
  block/post/template types, viewport width (previously lost on save),
  inserter visibility, and synced status all round-trip through the
  pattern file header. New Metadata panel; Bindings panel re-enabled as
  the slot-authoring UI.
- Conversions both ways: POST with fromWpBlock converts a user pattern to
  a theme file; PUT with source=user converts a theme pattern to a
  wp_block post (images exported to the media library).
- One-time migration: wp:block refs to 1.x mirror posts are rewritten to
  wp:pattern slugs in post content and theme files while the mirror rows
  still exist as the ID map; then the rows and 1.x capabilities are
  removed. uninstall.php added.
- Deleted: the tbell_pattern_block CPT, all four /wp/v2/blocks
  interception filters (including the unauthenticated delete and
  edit-context read paths), the per-request registry rewrite and DB
  upserts (TWE-369), and syncedPatternFilter.js.
- Fixes along the way: Pattern_Builder_Security namespace fatal, parent
  theme patterns ignored (TWE-373), create-time theme choice being
  cosmetic (TWE-370), sidebar not refreshing after create (TWE-374),
  broken drag payload, slug -x-x- encoding (TWE-371), user pattern list
  truncated at 10, and the fixture theme's template referencing patterns
  from the wrong namespace.
- Tests: REST suite rewritten for the new routes (auth included),
  migration suite added, the companion's runtime suites vendored alongside
  the runtime classes, JS tests for the override transforms. 92 PHP tests
  and 14 JS tests, verified in wp-env; editor flows verified in a real
  browser, alone and with the companion active.
- Requires WordPress 6.8 / PHP 7.4. Version 2.0.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

pbking commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

CI status: the claude-review check failed for infrastructure reasons, not because of this PR's changes.

The job errored before producing a review: API Error: 401 {"type":"authentication_error","message":"OAuth access token has been revoked."} (job log). The workflow's stored Anthropic OAuth token has been revoked — the last successful run of this workflow was in March; this PR is the first to trigger it since. Nothing in this diff touches the workflow or its credentials, and there's no in-repo fix I can port — a repository admin needs to refresh the claude-code-review workflow's OAuth token secret.

I'm re-running the check once to confirm it reproduces, and will keep the PR watched. For reference, the change itself is fully validated outside this check: 92 PHP tests / 276 assertions and 14 JS tests green in wp-env, phpcs/eslint/stylelint clean, and the editor flows verified in a real browser (details in the PR description).


Generated by Claude Code

pbking commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Correction to the above: I don't have permission to re-run the failed job (the API returns 403 for this integration), so the single confirming re-run isn't available to me. The failure is deterministic in any case — a revoked OAuth token will 401 on every run until the secret is refreshed. Once an admin rotates the token, re-running the claude-review job on this run (or pushing any new commit) will re-trigger it. I'll keep checking in on the PR.


Generated by Claude Code

Feedback round on the 2.0 editing surfaces:

- The Appearance → Pattern Builder page keeps its browse grid, but its
  edit mode now boots core's own edit-post editor
  (wp.editPost.initializeEditor) against the pb_pattern entity — the
  genuine post-editor chrome (header, list view, inspector, document
  panels, shortcuts) saving straight to the pattern file. The custom
  EditorProvider/BlockCanvas editor is deleted.
- A history.replaceState guard keeps the page's own URL (the editor
  assumes post.php, where a string id would 404), and a
  MainDashboardButton fill turns the fullscreen close button into a
  Back button that returns to the server-validated back URL.
- User patterns now stay in the Site Editor everywhere: in place via
  its /wp_block route when editing from within the Site Editor, and
  deep-linked into that canvas from the browse grid and anywhere else
  without an entity-swap callback.
- Theme patterns opened from the Site Editor carry a back URL, so the
  editor's Back button returns to the exact Site Editor screen. (The
  Site Editor canvas itself cannot bind a pb_pattern: core hard-codes
  its editable entity types and keeps route registration private.)
- The REST response now advertises wp:action-publish (and
  wp:action-unfiltered-html) links, so the editor's save button reads
  Save/Update instead of degrading to Submit for Review.

Verified in wp-env (Playwright): browse → theme pattern → real editor →
save writes the theme file with intact header, reload survives, Back
returns to origin; Site Editor → theme pattern round-trip; Site Editor →
user pattern stays in place; browse → user pattern lands in the Site
Editor canvas; post-editor in-context editing and override isolation
regressions pass. PHP suite 92 tests / 278 assertions; JS 14 tests;
eslint/stylelint/phpcs clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

- Unsynced → synced now saves for user patterns. Core registers the
  wp_pattern_sync_status meta with enum [partial, unsynced], so writing
  an empty string failed REST validation and the save 400ed. Synced is
  the absence of the meta; the panel now writes null, the REST meta
  API's delete.
- Inserting a synced USER pattern from the sidebar now lands as core's
  native synced block — core/block {ref} — instead of a core/pattern
  naming a slug the registry doesn't have (which rendered nothing).
  Synced THEME patterns keep inserting as core/pattern {slug}; the drag
  payload follows the same rule.
- New Edit Pattern button in the block toolbar of a synced theme
  pattern instance (core/pattern), next to the runtime's Reset/Detach.
  It renders only when the slug resolves to a pb_pattern entity and
  opens the pattern the way the sidebar's Edit does: in-context in the
  post editor, the Pattern Builder editor elsewhere. (Synced user
  instances are core/block, where core provides Edit original.)

Verified in wp-env (Playwright): synced user pattern inserts as
core/block ref 25 and renders in the canvas; the sync flip on an
unsynced wp_block saves and the collection endpoint reports it synced;
Edit Pattern appears on selection and swaps the pb_pattern entity
in-context. JS 14 tests, eslint/stylelint/phpcs clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

The Appearance → Pattern Builder browse mode becomes a Site-Editor-style
library:

- A category rail on the left — All patterns, My patterns (user-created),
  every category in use (registered labels when available), and
  Uncategorized — each with a count.
- Pattern cards render at a uniform 1:1 size (the preview is cropped to
  a square) with the title and source/sync badges beneath. Cards carry
  no actions: clicking selects.
- Selecting a pattern opens a details sidebar with the same panels the
  editor shows for a pattern document (Source with conversion, Synced
  Status, and for theme patterns Metadata and Associations), staged on
  the entity — plus Save (enabled when dirty, persists via the entity
  save flow) and Edit (opens the pattern's editor).
- Snackbar notices now render on the page, so save results and the
  conversion errors the panels already dispatched are visible.

Verified in wp-env (Playwright): rail counts and filtering, square
cards with no edit buttons, details panels for theme and user patterns,
Save disabled-when-clean / enabled-when-dirty, a saved description
landing in the pattern file, an unsynced user pattern flipped to synced
from the sidebar, and Edit opening the core editor. JS 14 tests,
eslint/stylelint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

Grid tracks are now minmax(200px, 250px) instead of stretching to fill
a sparse row, so pattern cards keep a consistent size at every screen
width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

The container was sized 100vh minus the admin bar, ignoring whatever the
admin renders above it (notices, update nags), so the page scrolled and
the bottom of the browser sat below the fold. The boot script now
measures the container's real offset and sizes it to end at the
viewport bottom, re-measuring on resize and whenever #wpbody-content's
height changes (a notice appearing or being dismissed — the admin body
itself keeps a viewport-locked height, so it's the wrong observe
target). The admin footer and bottom padding are suppressed on this
screen so nothing renders below the app.

Verified in wp-env: no page scroll and a flush bottom edge with no
notice, with a notice injected after load, and after dismissing it; the
grid pane keeps its internal scroll.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

The same release workflow synced-patterns-for-themes ships:

- bin/ship.sh — npm run plugin-ship (and :dry-run): preflight checks
  (version agreement across the plugin header, package.json, and
  readme.txt's Stable tag; clean git tree; free tag), builds, syncs the
  .distignore-defined ship set into svn/trunk via a clean staging hop,
  refreshes the wp.org assets, copies trunk to tags/{version}, and
  commits all of it in one go after confirmation. Differences from the
  companion's copy: the plugin slug, the built-bundle existence checks
  (three bundles instead of index.js), and a SHIP_SVN_URL override for
  rehearsing against a local SVN repository.
- bin/ship-reset.sh — npm run plugin-ship:reset, verbatim from the
  companion.
- .distignore — the ship set: build/, includes/, index.php,
  pattern-builder.php, readme.txt, uninstall.php.
- .wordpress-org/ — plugin icon (SVG plus 128/256 PNGs) in the
  companion's brand palette.

Rehearsed end-to-end against a local svnadmin repository: trunk ends up
holding exactly the ship set, assets and the 2.0.0 tag land, and the
release commits with 'Release 2.0.0 (git …)'. Versions already agree at
2.0.0 everywhere (header, PATTERN_BUILDER_VERSION, package.json,
Stable tag), so no bump was needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

The two-plugin coordination (Pattern Builder's owns_runtime class_exists
gate, the companion's provider-deference guard, the patternHostAmended
setValues marker) collapses into one check in one place: Synced
Patterns for Themes 2.0.1 now sees PATTERN_BUILDER_VERSION at
plugins_loaded and stays entirely unloaded. Pattern Builder always
registers the full stack, and the vendored runtime copies drop the
now-pointless guards to stay logic-identical to the companion's.

The companion cache flush becomes a direct transient delete — the
companion's classes are never loaded alongside, but its week-long
per-theme cache may survive from before, and clearing it on every file
write keeps the companion current if Pattern Builder is ever
deactivated.

Verified in wp-env with both plugins active: the companion's classes
are not loaded, this plugin's Pattern_Block owns the core/pattern
render callback, and insertion, canvas and front-end rendering, and
per-instance override isolation all pass; deactivating Pattern Builder
hands rendering back to the companion identically. PHP suite 91 tests /
276 assertions; JS 14 tests; lints clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

The 250px cap kept cards from getting smaller when the pane narrowed,
so only two fit beside the details sidebar. Tracks are now
minmax(160px, 1fr): more columns fit at any width, and the cards
stretch to share the row evenly instead of leaving a ragged gap.

Verified in wp-env: three columns with the details sidebar open (194px
cards), five without it (171px), previews square in both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@pbking
pbking marked this pull request as ready for review August 29, 2026 16:33
@pbking
pbking merged commit 8a46898 into main Aug 29, 2026
1 check failed
@pbking
pbking deleted the claude/pattern-builder-2-upgrade-t523cj branch August 29, 2026 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants