From superpowers-sage
Scaffolds new ACF Composer Gutenberg blocks in Sage themes with custom element architecture, scoped CSS, theme variations, block.json, Blade views, and JS lifecycle.
npx claudepluginhub codigodoleo/superpowers-sage --plugin superpowers-sageThis skill uses the workspace's default tool permissions.
Scaffold a new ACF block with the full custom element architecture: scoped CSS,
Refactors existing ACF Composer blocks along 4 axes: rendering model, field composition, variants/styles, InnerBlocks adoption. Migrates legacy v1-v3, classifies phases, generates reports without breaking content.
Generates WordPress Full Site Editing block themes: theme.json configs, block templates, template parts, patterns, functions.php, and styles.
Recommends and installs shadcn/ui components plus 1,338 premium ShadcnBlocks and 1,189 free components for React/Next.js + Tailwind frontends like landing pages, dashboards, heroes, and pricing sections.
Share bugs, ideas, or general feedback.
Scaffold a new ACF block with the full custom element architecture: scoped CSS, theme variations, optional JS lifecycle, selective enqueue, and documentation.
Announce at start: "I'm using the block-scaffolding skill to create the block."
/building: after lando acorn acf:block produces a new stub/block-refactoring instead.$ARGUMENTS
Resolve to {ClassName} (PascalCase) and {slug} (kebab-case) before proceeding.
# Generate block stub (PascalCase name required)
bash skills/block-scaffolding/scripts/create-block.sh HeroBlock
# → app/Blocks/HeroBlock.php
# → resources/views/blocks/hero-block.blade.php
P1. Slug produces a valid custom element tag — block-{slug}. The block-
prefix guarantees the required hyphen.
P2. Design system foundation:
resources/css/design-tokens.css — must exist
resources/views/components/ui/ — must contain button + heading
P3. BaseCustomElement in theme:
resources/js/core/BaseCustomElement.js — must exist
| Mode | When | $styles | CSS variations |
|---|---|---|---|
| Full | Theme switching (light/neutral/dark) | 3 entries | 3 .is-style-* selectors |
| Minimal | Fixed appearance (footer, nav, ticker) | absent | base tokens only |
lando acorn acf:block {ClassName} --localize
Produces:
app/Blocks/{ClassName}.php — controller stub (localization-ready)resources/views/blocks/{slug}.blade.php — view stubBefore writing the view, build a local registry of available shared components:
resources/views/components/*.blade.php — list all component files@props declaration → note prop names + defaults
b. Grep for var(--) patterns → note CSS variable names consumed{
"section-header": { props: ["eyebrow","title","align"] },
"eyebrow": { consumes: ["--eyebrow-color","--decorator-color"] },
"button": { consumes: ["--btn-bg","--btn-text"] }
}
This registry drives CSS generation in S1 — variable names come from the project, not from the skill's assumptions.
Rule: if the block needs eyebrow + heading markup, use <x-section-header> (or
the equivalent shared component) — do NOT emit <x-eyebrow> + <h2> inline.
If no suitable component exists, use inline markup and note it for future extraction.
Detect whether the block embeds an HTML Forms form. Two signals — any match triggers:
form, formulário, contact form, contato, html forms (case-insensitive)fields() for this block includes an addPostObject with post_type containing html-formIf triggered, load the sage-forms skill and its references (blade-form-views.md, hf-validation.md, traps.md) before continuing. After Phase 1 S2 (controller written), run the three coordinated scaffolds below. If not triggered, skip this phase entirely.
resources/views/forms/{form-slug}.blade.php)Resolve {form-slug}:
html-form post (e.g. "contact form" → contact), use that slug.{slug}-form as placeholder and prepend {{-- TODO: rename file to match the html-form CPT post_name --}} at the top.Write the form view using <x-html-forms> + x-form.* components, with one x-form.field per ACF field declared in S2. Submit button is <x-button type="submit">. Do not pass pattern attributes; do not use type="tel" (use type="text" inputmode="tel" for phone fields). See skills/sage-forms/references/blade-form-views.md for the full pattern and references/traps.md for the rationale.
resources/js/modules/hf-validation.js)Glob check: if resources/js/modules/hf-validation.js exists, skip — one module per project, reused across forms. If absent, write the scaffold from skills/sage-forms/references/hf-validation.md (the "Full Module Skeleton" section).
resources/js/blocks/{slug}.js)Add an import at the top:
import { initHfValidation } from '../modules/hf-validation';
Inside the block's init() method, add:
const form = this.querySelector('.hf-form');
if (form) {
initHfValidation(form, {
messages: {
// TODO: configure per form — see skills/sage-forms/references/hf-validation.md
},
validators: {
// TODO: configure per form
},
});
}
Phase 0c does NOT write validator functions or localized messages — always produces // TODO: configure per form stubs. Validator content varies per project and per form; this is deliberate.
resources/css/blocks/{slug}.cssBefore generating CSS, apply the background context decision table.
Read the component's design-guide.md (## Tokens → Colors section) if available:
| Token found in design-guide Colors | Background context | CSS action |
|---|---|---|
bg-depth, bg-primary, bg-dark, bg-inverse | Dark | Override cascade vars with *-on-dark equivalents |
bg-identity, bg-sage, bg-accent | Identity (brand color bg) | Override cascade vars with *-on-identity equivalents (e.g. var(--color-identity-fg)) |
bg-bg, bg-surface, bg-muted, absent | Light (default) | No override — inherit :root defaults |
| Unrecognized token | Ambiguous | Generate with /* VERIFY: background context unknown */ |
Variable names for the cascade block come from the Phase 0b registry (what each
shared component actually consumes via var(--) references).
Full mode — light section (no override):
@reference "../app.css";
block-{slug} {
@apply block overflow-hidden;
/* cascade — inherited by child components */
--eyebrow-color: var(--color-identity);
--heading-color: var(--color-fg);
--body-color: var(--color-fg);
--decorator-color: var(--color-identity);
}
(variable names from Phase 0b registry; values from :root defaults)
Full mode — dark section (bg-depth detected in design-guide):
@reference "../app.css";
block-{slug} {
@apply block overflow-hidden;
/* cascade — dark section, override :root defaults; values may differ per component — adjust per design-guide */
--eyebrow-color: var(--color-depth-fg);
--heading-color: var(--color-depth-fg);
--body-color: var(--color-depth-fg);
--decorator-color: var(--color-depth-fg);
}
Full mode with $styles variations (background changes per variation):
@reference "../app.css";
block-{slug} {
@apply block overflow-hidden;
/* cascade — light default */
--eyebrow-color: var(--color-identity);
--heading-color: var(--color-fg);
--body-color: var(--color-fg);
--decorator-color: var(--color-identity);
}
.is-style-dark block-{slug} {
--eyebrow-color: var(--color-depth-fg);
--heading-color: var(--color-depth-fg);
--body-color: var(--color-depth-fg);
--decorator-color: var(--color-depth-fg);
}
Minimal mode: omit .is-style-* selectors. Include the token declarations
commented out so the developer has the vocabulary available:
@reference "../app.css";
block-{slug} {
@apply block overflow-hidden;
/* --eyebrow-color: var(--color-identity); */
/* --heading-color: var(--color-fg); */
/* --body-color: var(--color-fg); */
/* --decorator-color: var(--color-identity); */
}
CSS rules:
@apply for all Tailwind utilities (block, overflow-hidden, flex, spacing, etc.)@apply equivalent exists for cascade variables@theme tokens via var(--)@reference not @import — grants token access without duplicating the stylesheet.is-style-* block-{slug} single selector — works in editor and frontendapp/Blocks/{ClassName}.phpUse template: assets/block-atomic.php.tpl (leaf block) or assets/block-container.php.tpl (InnerBlocks).
Key rules:
$spacing and $supports — editor controls users expect$styles: Full mode only — omit entirely for Minimal blocks$styles format: ['name' => 'dark'] not ['value' => 'dark'] (WP 6.x)assets() must remain empty — enqueue via ThemeServiceProvider::boot()fields() declares all ACF fields — never use the ACF GUISee references/acf-composer-registration.md for full class structure.
For InnerBlocks containers see references/inner-blocks.md.
For $styles and variant CSS see references/variants.md.
resources/js/blocks/{slug}.jsAlways generate, even for static blocks:
import BaseCustomElement from '../core/BaseCustomElement.js';
export default class Block{PascalSlug} extends BaseCustomElement {
static tagName = 'block-{slug}';
init() {
// Block behavior. Empty is valid for static blocks.
}
}
BaseCustomElement.register(Block{PascalSlug});
Rules: class name Block{PascalSlug}, static tagName matches CSS selector,
init() empty = static block, BaseCustomElement.register() at bottom.
resources/views/blocks/{slug}.blade.phpUse template: assets/block-view.blade.php.tpl. Key points:
@unless ($block->preview)
<section {!! get_block_wrapper_attributes() !!}>
@endunless
<block-{slug} class="flex flex-col">
{{-- content --}}
</block-{slug}>
@unless ($block->preview)
</section>
@endunless
Rules:
@unless ($block->preview) wraps <section> — skipped in editor<block-{slug}> is the CSS and JS root<block-{slug}> or childrenFor editor vs frontend parity issues see references/edit-preview-parity.md.
For block.json and editor_script see references/block-json.md.
ThemeServiceProvider::boot())Search for has_block in app/Providers/ThemeServiceProvider.php.
If pattern EXISTS: add '{slug}' => true, to the $blocks array.
If pattern DOES NOT EXIST: implement it:
add_action('wp_enqueue_scripts', function () {
$blocks = [
'{slug}' => true,
];
foreach (array_keys($blocks) as $slug) {
if (! has_block("acf/{$slug}")) continue;
$cssAsset = \Roots\asset("css/blocks/{$slug}.css");
if ($cssAsset->exists()) {
wp_enqueue_style("block-{$slug}", $cssAsset->uri(), [], $cssAsset->version());
}
$jsAsset = \Roots\asset("js/blocks/{$slug}.js");
if ($jsAsset->exists()) {
wp_enqueue_script("block-{$slug}", $jsAsset->uri(), [], $jsAsset->version(), true);
}
}
}, 20);
Add one @import per block to resources/css/editor.css:
@import './blocks/{slug}.css';
docs/blocks/{slug}.md)Document: custom element name, ACF fields table, theme variations table (Full mode), CSS tokens table, and file dependency list (controller, view, CSS, JS, enqueue, editor CSS).
If Phase 0c triggered, the README additionally documents:
resources/views/forms/{form-slug}.blade.phpresources/js/modules/hf-validation.jshf-success, hf-errorsage-forms skill for the integration patternlando theme-build # exit 0; block-{slug}-*.css AND .js must appear in output
lando flush # clear Acorn/Blade/OPcache
plan.md<link href="*/block-{slug}-*.css"> and <script> in DOMdocument.querySelector('block-{slug}').constructor !== HTMLElementis-style-neutral / is-style-dark
to the outer <section> and confirm cascade vars (e.g. --eyebrow-color, --heading-color) resolve correctlygit commit -m "feat(blocks): scaffold {slug}" && git pushlando acorn acf:block — never create block stubs manually. The generator sets correct namespaces, registration hooks, and file paths.resources/views/blocks/{slug}.blade.php). Never plain PHP.fields() method — never the ACF GUI. GUI fields are lost on composer install.get_block_wrapper_attributes() on <section> — provides accessibility, spacing, alignment, and variation classes.assets() stays empty — CSS/JS enqueue belongs in ThemeServiceProvider::boot().@reference not @import in block CSS — avoids duplicating the full stylesheet.@apply block overflow-hidden on the custom element — custom elements default to inline; overflow-hidden prevents bleed.| Wrong | Correct |
|---|---|
<section class="b-{slug}"> + $attributes->merge() | @unless preview <section {!! get_block_wrapper_attributes() !!}> + <block-{slug}> |
.b-{slug} { ... } | block-{slug} { ... } tag selector |
&.is-style-neutral, .is-style-neutral & | .is-style-neutral block-{slug} |
No @apply block on custom element | @apply block overflow-hidden always — custom elements default to inline |
<hero> (no hyphen) | <block-hero> |
assets() with enqueue | assets() empty — enqueue in ThemeServiceProvider |
@import "../app.css" | @reference "../app.css" |
['value' => 'dark'] in $styles | ['name' => 'dark'] |
Skip $spacing / $supports | Always declare both |
acf:block without --localize | lando acorn acf:block {Name} --localize |
| Skip JS for static block | Generate stub with empty init() |
| Done without git commit | git commit + git push required |
lando theme-build exits 0<link> and <script> for block assets in frontend <head>HTMLElement)lando wp --info outputgit commit made with feat(blocks): scaffold {slug}Cause: Block CSS not imported in editor.css or Vite entrypoint.
Fix: Add @import './blocks/{slug}.css' to resources/css/editor.css.
Cause: JS not enqueued, or BaseCustomElement.register() missing.
Fix: Verify ThemeServiceProvider::boot() includes the slug and the JS file exists.
Cause: <section {!! $attributes !!}> missing — is-style-* class not in DOM.
Fix: Restore @unless ($block->preview) wrapper with get_block_wrapper_attributes().
Cause: Fields defined in fields() but block registered before ACF Composer boots.
Fix: Ensure the block class is auto-discovered by ACF Composer (check config/acf.php).
For all edit/preview parity issues see references/edit-preview-parity.md.