Files
CWC/docs/superpowers/plans/2026-07-25-team-grid-block.md
T

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.css is committed (not gitignored) per project convention.
  • PHPCS uses the WordPress coding standard; composer lint must 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-md to 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, or views/partials/page-hero-services.php.
  • page.php is not modified.
  • No JavaScript files are added.
  • Reuse the same regACFBlocks() auto-registration pattern as services-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 to block == 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.json declaring acf/team-grid as the block name, team-grid.php as the render template, groups icon, common category. regACFBlocks() in functions.php will 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. Reuses text-cwc-blue-01 for the name. Implements the grid-template-rows: 0fr → 1fr animation 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 with image (array: url, alt), name (string), title (string), bio (wysiwyg HTML).

  • Produces: A <section class="team-grid ..."> element with a __inner grid div, and an <article class="team-grid__card"> per member. Each card contains a <details> with a <summary> (photo + name + title) and a __bio-wrap containing 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 @import line at the end of styles/blocks/index.css so 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.css that includes the .team-grid* rules and the responsive grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 utilities.

  • 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.php template (Tasks 3 + 4) on the WordPress page /team/.
  • Produces: Three tests:
    1. Photo, name, and title visible for the first card.
    2. Bio is closed by default; clicking the summary opens the details and reveals the bio; clicking again closes it.
    3. 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 Grid block 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 members with image / 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 → 1fr animation — 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.php is 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.
  • 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 names image, name, title, bio are used in Task 1 (defined) and Task 4 (consumed). Class names team-grid, __inner, __card, __details, __summary, __photo, __name, __title, __bio-wrap, __bio-inner, __bio are 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 as grid-template-rows: 0fr → 1fr with 300ms transition. "Responsive grid" is concretely specified as grid-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.