23 KiB
Team Grid Block Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a new team-grid ACF block for the Team page (and any other page that needs a team grid) that renders a responsive grid of team members. Each member shows their photo (rounded rectangle), name, and title. The bio is collapsed by default and expands vertically with a smooth animation when the photo, name, or title is clicked.
Architecture: One new ACF block (acf/team-grid) with a single repeater of members (image, name, title, bio). The block uses native HTML <details>/<summary> for the click-to-expand interaction, animated with the grid-template-rows: 0fr → 1fr CSS pattern (no JavaScript). The block has no built-in background — the editor wraps it in an existing Section block when a dark/light background is needed. The page template is unchanged; the editor adds the block to the Team page in Gutenberg.
Tech Stack: WordPress 6.x, PHP 8.x, Tailwind CSS v4, Playwright, axe-core, PHPCS (WordPress standard).
Global Constraints
- Tabs for PHP indentation (project standard).
static/dist/theme.cssis committed (not gitignored) per project convention.- PHPCS uses the WordPress coding standard;
composer lintmust pass for all touched files. - All design colors use the project's existing CSS custom properties from
styles/base/colors.css. No hardcoded hex values for theme colors. - Reuse existing design tokens where possible (e.g.
text-cwc-blue-01,var(--color-secondary)). - The headshot uses
rounded-mdto match other image cards in the project. - All commits follow the project's emoji-prefixed commit-message style (e.g.
✨ feat: …). - This work does not edit
header.php,footer.php,views/partials/page-hero.php, orviews/partials/page-hero-services.php. page.phpis not modified.- No JavaScript files are added.
- Reuse the same
regACFBlocks()auto-registration pattern asservices-list. - Tailwind v4 picks up class names from source files at build time. The dynamic class names used in the CSS (
grid-cols-1,sm:grid-cols-2,lg:grid-cols-3,w-32 h-32, etc.) must all be literal in the source for the build to emit them.
File Structure
| File | Responsibility | Created/Modified |
|---|---|---|
views/blocks/team-grid/team-grid.php |
Block template (PHP) — renders the grid. | Create |
views/blocks/team-grid/team-grid.css |
Block styles (CSS) — layout, photo, summary, animation. | Create |
views/blocks/team-grid/block.json |
Block registration metadata. | Create |
acf/group_team_grid.json |
ACF field group for the acf/team-grid block. |
Create |
styles/blocks/index.css |
Import the new block's CSS. | Modify |
tests/team-page.spec.js |
Playwright tests for the rendered block. | Create |
static/dist/theme.css |
Rebuilt via npm run build. |
Modified |
Task 1: ACF Field Group
Files:
- Create:
acf/group_team_grid.json
Interfaces:
-
Produces: ACF group with
key: "group_team_grid", location scoped toblock == acf/team-grid, fields:members(repeater) →image,name,title,bio. -
Step 1: Create the ACF field group JSON file
Create the file acf/group_team_grid.json with the following exact content:
{
"key": "group_team_grid",
"title": "Team Grid",
"fields": [
{
"key": "field_team_grid_members",
"label": "Members",
"name": "members",
"aria-label": "",
"type": "repeater",
"instructions": "Add team members. Click a card to expand the bio.",
"required": 0,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"layout": "block",
"pagination": 0,
"min": 0,
"max": 0,
"collapsed": "",
"button_label": "Add Team Member",
"rows_per_page": 20,
"sub_fields": [
{
"key": "field_team_grid_member_image",
"label": "Image",
"name": "image",
"aria-label": "",
"type": "image",
"instructions": "",
"required": 0,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"return_format": "array",
"library": "all",
"min_width": "",
"min_height": "",
"min_size": "",
"max_width": "",
"max_height": "",
"max_size": "",
"mime_types": "",
"allow_in_bindings": 0,
"preview_size": "medium"
},
{
"key": "field_team_grid_member_name",
"label": "Name",
"name": "name",
"aria-label": "",
"type": "text",
"instructions": "",
"required": 0,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"default_value": "",
"maxlength": "",
"allow_in_bindings": 0,
"placeholder": "",
"prepend": "",
"append": ""
},
{
"key": "field_team_grid_member_title",
"label": "Title",
"name": "title",
"aria-label": "",
"type": "text",
"instructions": "",
"required": 0,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"default_value": "",
"maxlength": "",
"allow_in_bindings": 0,
"placeholder": "",
"prepend": "",
"append": ""
},
{
"key": "field_team_grid_member_bio",
"label": "Bio",
"name": "bio",
"aria-label": "",
"type": "wysiwyg",
"instructions": "",
"required": 0,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"default_value": "",
"tabs": "visual",
"toolbar": "basic",
"media_upload": 0,
"delay": 0,
"allow_in_bindings": 0
}
]
}
],
"location": [
[
{
"param": "block",
"operator": "==",
"value": "acf/team-grid"
}
]
],
"menu_order": 0,
"position": "normal",
"style": "default",
"label_placement": "top",
"instruction_placement": "label",
"hide_on_screen": "",
"active": true,
"description": "Team grid block. Each member is a card with photo, name, title, and a collapsible bio.",
"show_in_rest": 0,
"display_title": "",
"allow_ai_access": false,
"ai_description": "",
"modified": 1753401600
}
- Step 2: Verify the JSON parses
Run: node -e "JSON.parse(require('fs').readFileSync('acf/group_team_grid.json','utf8')); console.log('OK')"
Expected: prints OK and exits 0.
- Step 3: Commit
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add acf/group_team_grid.json
git commit -m "✨ feat: Add ACF field group for team-grid block"
Task 2: Block Registration JSON
Files:
- Create:
views/blocks/team-grid/block.json
Interfaces:
-
Produces:
block.jsondeclaringacf/team-gridas the block name,team-grid.phpas the render template,groupsicon,commoncategory.regACFBlocks()infunctions.phpwill pick this up automatically. -
Step 1: Create the block.json file
Create the file views/blocks/team-grid/block.json with the following exact content:
{
"name": "acf/team-grid",
"title": "Team Grid",
"description": "A responsive grid of team members. Click a card to expand the bio.",
"category": "common",
"icon": "groups",
"keywords": ["team", "people", "staff", "bio"],
"acf": {
"mode": "preview",
"renderTemplate": "team-grid.php"
},
"supports": {
"anchor": true,
"align": false
}
}
- Step 2: Verify the JSON parses
Run: node -e "JSON.parse(require('fs').readFileSync('views/blocks/team-grid/block.json','utf8')); console.log('OK')"
Expected: prints OK and exits 0.
- Step 3: Commit
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add views/blocks/team-grid/block.json
git commit -m "✨ feat: Add team-grid block.json registration"
Task 3: Block CSS
Files:
- Create:
views/blocks/team-grid/team-grid.css
Interfaces:
-
Produces: BEM-styled CSS for
.team-grid,__inner,__card,__details,__summary,__photo,__name,__title,__bio-wrap,__bio-inner,__bio. Reusestext-cwc-blue-01for the name. Implements thegrid-template-rows: 0fr → 1franimation for the bio. -
Step 1: Create the block CSS file
Create the file views/blocks/team-grid/team-grid.css with the following exact content:
/* Team Grid block */
.team-grid {
&__card {
@apply flex flex-col;
}
&__details {
@apply flex flex-col;
}
&__summary {
@apply flex flex-col items-center text-center cursor-pointer list-none;
&::-webkit-details-marker {
display: none;
}
&::marker {
display: none;
content: '';
}
}
&__photo {
@apply w-32 h-32 sm:w-40 sm:h-40 lg:w-48 lg:h-48 object-cover rounded-md mb-4;
}
&__name {
@apply text-cwc-blue-01 text-22px font-bold leading-tight mb-1;
}
&__title {
@apply text-gray-600 text-16px font-light italic mb-3;
}
&__bio-wrap {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms ease;
}
&__details[open] &__bio-wrap {
grid-template-rows: 1fr;
}
&__bio-inner {
overflow: hidden;
min-height: 0;
}
&__bio {
@apply text-gray-700 text-16px font-light leading-snug pt-2;
}
}
- Step 2: Verify the BEM children are present
Run: grep -cE "&__(card|details|summary|photo|name|title|bio-wrap|bio-inner|bio)\b" views/blocks/team-grid/team-grid.css
Expected: 9 (one match per BEM child).
- Step 3: Commit
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add views/blocks/team-grid/team-grid.css
git commit -m "✨ feat: Add team-grid block CSS"
Task 4: Block Template (PHP)
Files:
- Create:
views/blocks/team-grid/team-grid.php
Interfaces:
-
Consumes:
get_field( 'members' )→ array of members each withimage(array:url,alt),name(string),title(string),bio(wysiwyg HTML). -
Produces: A
<section class="team-grid ...">element with a__innergrid div, and an<article class="team-grid__card">per member. Each card contains a<details>with a<summary>(photo + name + title) and a__bio-wrapcontaining the bio. -
Step 1: Create the block PHP template
Create the file views/blocks/team-grid/team-grid.php with the following exact content (tabs for indentation):
<?php
/**
* Block Name: Team Grid
*
* A responsive grid of team members. Each card uses a native <details>
* element so the bio expands when the photo, name, or title is clicked.
*
* @package CWC
*/
namespace CWC;
$members = get_field( 'members' );
if ( empty( $members ) ) {
return;
}
$classes = 'team-grid container my-section mx-auto';
$wrapper = blockWrapperAttributes( $classes, $is_preview );
?>
<section <?php echo wp_kses_post( $wrapper ); ?>>
<div class="team-grid__inner grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-12">
<?php foreach ( $members as $member ) : ?>
<article class="team-grid__card">
<details class="team-grid__details">
<summary class="team-grid__summary">
<?php if ( ! empty( $member['image'] ) ) : ?>
<img
class="team-grid__photo"
src="<?php echo esc_url( $member['image']['url'] ); ?>"
alt="<?php echo esc_attr( $member['image']['alt'] ?? '' ); ?>"
loading="lazy"
/>
<?php endif; ?>
<?php if ( ! empty( $member['name'] ) ) : ?>
<h3 class="team-grid__name">
<?php echo wp_kses_post( $member['name'] ); ?>
</h3>
<?php endif; ?>
<?php if ( ! empty( $member['title'] ) ) : ?>
<p class="team-grid__title">
<?php echo wp_kses_post( $member['title'] ); ?>
</p>
<?php endif; ?>
</summary>
<div class="team-grid__bio-wrap">
<div class="team-grid__bio-inner">
<?php if ( ! empty( $member['bio'] ) ) : ?>
<div class="team-grid__bio">
<?php echo wp_kses_post( $member['bio'] ); ?>
</div>
<?php endif; ?>
</div>
</div>
</details>
</article>
<?php endforeach; ?>
</div>
</section>
- Step 2: Lint the PHP file with PHPCS
Run: composer exec phpcs -- views/blocks/team-grid/team-grid.php
Expected: exits 0 with no errors. If PHPCS reports alignment warnings on the assignment block (similar to the Task 5 services-list fix), run composer exec phpcbf -- views/blocks/team-grid/team-grid.php to auto-fix, then re-run PHPCS to confirm exit 0. Whitespace-only changes are acceptable; do not modify logic.
- Step 3: Verify the dynamic utility classes are in the source
Run: grep -E "grid-cols-1|sm:grid-cols-2|lg:grid-cols-3|w-32|w-40|w-48" views/blocks/team-grid/team-grid.php
Expected: at least 3 matches (Tailwind v4 must see the literal class names in the source to emit the rules).
- Step 4: Commit
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add views/blocks/team-grid/team-grid.php
git commit -m "✨ feat: Add team-grid block PHP template"
Task 5: CSS Import Wiring
Files:
- Modify:
styles/blocks/index.css
Interfaces:
-
Consumes:
views/blocks/team-grid/team-grid.css(from Task 3). -
Produces: A new
@importline at the end ofstyles/blocks/index.cssso Tailwind's build picks up the block's CSS. -
Step 1: Add the import to the block index CSS
Append the following line at the end of styles/blocks/index.css:
@import '../../views/blocks/team-grid/team-grid.css';
After the edit, the file should end with:
@import '../../views/blocks/services-list/services-list.css';
@import '../../views/blocks/team-grid/team-grid.css';
- Step 2: Verify the import is present
Run: grep -cF "team-grid/team-grid.css" styles/blocks/index.css
Expected: 1.
- Step 3: Commit
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add styles/blocks/index.css
git commit -m "🔧 chore: Import team-grid block CSS into theme"
Task 6: Build the CSS
Files:
- Modify:
static/dist/theme.css
Interfaces:
-
Consumes: The new import in
styles/blocks/index.css(from Task 5) and the block CSS (from Task 3). -
Produces: A rebuilt
static/dist/theme.cssthat includes the.team-grid*rules and the responsivegrid-cols-1 sm:grid-cols-2 lg:grid-cols-3utilities. -
Step 1: Run the production Tailwind build
Run: npm run build
Expected: completes with no errors.
- Step 2: Verify the team-grid CSS classes are in the built output
Tailwind v4 with --optimize preserves CSS-nesting rather than unwrapping &__name into __name.team-grid. Use fixed-string search (Windows-safe) that matches the preserved form:
grep -cF "&__name" static/dist/theme.css
Expected: 1 or more (the &__name rule is nested inside .team-grid { ... } in the dist).
- Step 3: Verify the responsive grid utilities are in the built output
Same Windows-safe form:
grep -cF "sm\:grid-cols-2" static/dist/theme.css
grep -cF "lg\:grid-cols-3" static/dist/theme.css
Expected: each returns 1 or more.
- Step 4: Commit the rebuilt dist
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add -f static/dist/theme.css
git commit -m "🎨 build: Rebuild dist with team-grid block styles"
Task 7: Playwright Tests
Files:
- Create:
tests/team-page.spec.js
Interfaces:
- Consumes: The rendered
views/blocks/team-grid/team-grid.phptemplate (Tasks 3 + 4) on the WordPress page/team/. - Produces: Three tests:
- Photo, name, and title visible for the first card.
- Bio is closed by default; clicking the summary opens the details and reveals the bio; clicking again closes it.
- No axe-core a11y violations scoped to
.team-grid.
Prerequisite: the test depends on the Team page being composed in Gutenberg with at least one
Team Gridblock containing one or more members. That composition is the editor's responsibility and is not part of this plan. The tests should be authored against the intended state, not a placeholder state.
- Step 1: Create the test spec file
Create the file tests/team-page.spec.js with the following exact content:
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test.describe("Team Grid block on /team/", () => {
test("renders the photo, name, and title for each member", async ({ page }) => {
await page.goto("/team/");
const cards = page.locator(".team-grid__card");
await expect(cards.first()).toBeVisible();
const firstCard = cards.first();
await expect(firstCard.locator(".team-grid__photo")).toBeVisible();
await expect(firstCard.locator(".team-grid__name")).toBeVisible();
await expect(firstCard.locator(".team-grid__title")).toBeVisible();
});
test("bio is hidden by default and expands on summary click", async ({ page }) => {
await page.goto("/team/");
const firstDetails = page.locator(".team-grid__details").first();
// Bio is closed by default.
await expect(firstDetails).not.toHaveAttribute("open", "");
// Click the summary; the details should open and the bio should be visible.
await firstDetails.locator(".team-grid__summary").click();
await expect(firstDetails).toHaveAttribute("open", "");
await expect(firstDetails.locator(".team-grid__bio")).toBeVisible();
// Click again; the details should close.
await firstDetails.locator(".team-grid__summary").click();
await expect(firstDetails).not.toHaveAttribute("open", "");
});
test("no axe violations on the team-grid block", async ({ page }) => {
await page.goto("/team/");
const results = await new AxeBuilder({ page })
.include(".team-grid")
.analyze();
expect(results.violations).toEqual([]);
});
});
- Step 2: Run the tests
Run: npx playwright test tests/team-page.spec.js
Expected: all 3 tests pass. If tests fail because the Team page has not yet been composed in the editor, that's an expected prerequisite — note the prerequisite in the final report and skip the test run.
- Step 3: Commit
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add tests/team-page.spec.js
git commit -m "✅ test: Add Playwright tests for team-grid block"
Self-Review Notes
- Spec coverage:
- One new ACF block (
team-grid) — covered by Tasks 1, 2, 3, 4. - Repeater of
memberswithimage/name/title/bio— covered by Task 1. - Responsive grid
grid-cols-1 sm:grid-cols-2 lg:grid-cols-3— covered by Task 4 (PHP) and Task 6 (Tailwind emits the rules). - Rounded-rectangle headshots via
rounded-md— covered by Task 3 (CSS). - Native
<details>/<summary>click-to-expand — covered by Task 4 (PHP) and Task 3 (CSS). grid-template-rows: 0fr → 1franimation — covered by Task 3 (CSS).- No built-in background — explicitly out of scope (Section block wrapper, editor's responsibility).
- Page template not modified — confirmed;
page.phpis not in any file list. header.php/footer.php/ page hero not touched — confirmed.- Reuse existing design tokens (
text-cwc-blue-01,rounded-md) — covered by Tasks 1, 3, 4. - Playwright tests covering photo + name + title, summary click toggles, and a11y — covered by Task 7.
- Dist rebuild — covered by Task 6.
- One new ACF block (
- Placeholder scan: No TBDs. Every step has a concrete file path, exact content, exact command, and expected output. The only conditional is in Task 7 Step 2 — explicitly called out as a prerequisite (editor must compose the page in Gutenberg), not a placeholder.
- Type consistency: ACF field
name: "members"is used in Task 4 (get_field( 'members' )). Sub-field namesimage,name,title,bioare used in Task 1 (defined) and Task 4 (consumed). Class namesteam-grid,__inner,__card,__details,__summary,__photo,__name,__title,__bio-wrap,__bio-inner,__bioare used in Task 3 (defined in CSS) and Task 4 (rendered in markup) and Task 7 (asserted in tests) — consistent. - Ambiguity check: "Rounded rectangle" is concretely specified as
rounded-md. "Opens vertically" is concretely specified asgrid-template-rows: 0fr → 1frwith 300ms transition. "Responsive grid" is concretely specified asgrid-cols-1 sm:grid-cols-2 lg:grid-cols-3. "No built-in background" is concretely scoped to the Section block wrapper (editor's responsibility, not this block's). All are explicit.