# Team Grid Block Design > **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 `
`/`` 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. the `text-cwc-blue-01` heading color, `var(--color-secondary)` accent if needed). - The headshot uses `rounded-md` to match other image cards in the project (`post-list`, `services-list`). - 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. The `
`/`` interaction is fully native. - Reuse the same `regACFBlocks()` auto-registration pattern as `services-list`. ## Reference Mockup `team-example.png` shows a dark-blue section with 8 team members in a 2-column grid. Each member has a circular headshot, name (bold), title (italic), and a multi-line bio below. All bios are visible in the mockup. **Intent of the design (per the user):** - The bio should be a `
` element that **opens vertically** when the photo, name, or title is clicked. The mockup shows the open state for every member for context; the live behavior is collapsed by default. - The headshot should be a **rectangle with rounded corners that match other images in the project** — `rounded-md` is the project standard. The mockup's circular headshots are a visual approximation; the live design uses rounded rectangles. - The block has no built-in background. The editor wraps it in a Section block (`acf/section`) when a dark or light background is needed. - Layout is responsive: 1 column on mobile, 2 on tablet, 3 on desktop (the user's "3-2-1" choice). ## File Structure | File | Responsibility | Created/Modified | | --- | --- | --- | | `acf/group_team_grid.json` | ACF field group for the `acf/team-grid` block. | Create | | `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 | | `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 | --- ## Design Details ### 1. ACF field group: `acf/team-grid` **Block name (registration key):** `acf/team-grid` **Editor title:** "Team Grid" **Category:** common **Description (in block.json):** "A responsive grid of team members. Click a card to expand the bio." #### ACF field group (`acf/group_team_grid.json`) ``` Group key: group_team_grid Group title: Team Grid Position: normal Menu order: 0 Location: block == acf/team-grid ``` Fields: - `members` (repeater, layout: block, button label: "Add Team Member") - `image` (image, return_format: array, required: 0) — Headshot. - `name` (text, required: 0) — Person's name (e.g. "Christopher Werner"). - `title` (text, required: 0) — Person's role (e.g. "Principal Consultant & CEO"). - `bio` (wysiwyg, tabs: visual, toolbar: basic, media_upload: 0, required: 0) — Bio body copy. The block is intentionally minimal. No per-card size, no global layout toggle, no CTA field, no group-level background color. Layout derives from CSS (`grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`). #### Block registration (`views/blocks/team-grid/block.json`) ```json { "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 } } ``` The `icon: "groups"` is a WordPress Dashicon that visually represents a group of people. #### Block template (`views/blocks/team-grid/team-grid.php`) ```php * 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 ); ?>
>
<?php echo esc_attr( $member['image']['alt'] ?? '' ); ?>

``` Notes: - Native `
`/``: the summary is the click target (photo + name + title); the body is the bio wrap. - The `
` element is closed by default — no `open` attribute. The user opens it on click. - The bio uses `wysiwyg` so editors can add links or basic formatting. Output is sanitized via `wp_kses_post`. - `team-grid__bio-wrap` is a 1-row grid container that animates `grid-template-rows` from `0fr` to `1fr`. The `__bio-inner` inside has `overflow: hidden; min-height: 0` so the bio is clipped during the transition. #### Block styles (`views/blocks/team-grid/team-grid.css`) ```css /* Team Grid block */ .team-grid { &__card { @apply flex flex-col; } &__details { @apply flex flex-col; } /* summary is the click target. Strip the default disclosure triangle. */ &__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; } /* Animation: animate grid-template-rows 0fr -> 1fr. */ &__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; } } ``` Notes: - The summary is a flex column with `items-center` so the photo, name, and title stack centered. This matches the mockup. - The default `` marker (▶ triangle) is hidden with `list-style: none` plus vendor-prefixed `::-webkit-details-marker` and `::marker` reset. - The `&__details[open] &__bio-wrap` selector uses CSS nesting to target the bio wrap when the parent details is open. This compiles to `.team-grid__details[open] .team-grid__bio-wrap { grid-template-rows: 1fr; }`. - The `team-grid__details[open] &__bio-wrap` syntax uses CSS nesting: `&` inside `&__details[open]` is the previous compound selector. Tailwind v4 (lightningcss) unwraps this correctly. - Photo sizes: `w-32 h-32` (8rem / 128px) on mobile, `w-40 h-40` (10rem / 160px) on small, `w-48 h-48` (12rem / 192px) on large. ### 2. CSS import wiring (`styles/blocks/index.css`) Append at the end of `styles/blocks/index.css`: ```css @import '../../views/blocks/team-grid/team-grid.css'; ``` After the edit, the file ends with: ``` @import '../../views/blocks/services-list/services-list.css'; @import '../../views/blocks/team-grid/team-grid.css'; ``` ### 3. Page integration `page.php` is unchanged. The editor adds a `Team Grid` block to the Team page (`/team/`) in Gutenberg. To get the dark-blue background, the editor wraps the `Team Grid` block in a `Section` block (`acf/section`) and sets the Section's `background_color` to the dark blue. No PHP template change is required. ### 4. CSS Build Run `npm run build` to compile Tailwind v4 → `static/dist/theme.css`. The new utilities (e.g. `text-22px`, `text-16px`, `w-32 h-32`, `rounded-md`) must appear in the dist. ### 5. Tests (`tests/team-page.spec.js`) ```js 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([]); }); }); ``` ### Out of Scope - The Team page template — `page.php` is unchanged. The editor adds the block in Gutenberg. - The Section block wrapper for the dark-blue background — the editor composes this in Gutenberg. - A custom open/close icon or arrow indicator — the click target is the entire summary (photo + name + title), so no chevron is needed. - Animations beyond the bio's expand/collapse transition. No hover transforms, no entry animations. - Per-card size variants — all cards are the same size. - A `data-team-member-id` slug for deep-linking to a specific member's expanded bio. - Dark-mode overrides — the block is color-agnostic and inherits the parent Section's color scheme. - Per-page header/footer changes. ## Self-Review Notes - **Spec coverage:** One new ACF block (4 files), one CSS import, dist rebuild, one test file. Every spec section is covered. - **Placeholder scan:** No TBDs. Every step has a concrete file path, exact content, exact command, and expected output. - **Type consistency:** Class names use the existing BEM convention (`__card`, `__details`, `__summary`, `__photo`, `__name`, `__title`, `__bio-wrap`, `__bio-inner`, `__bio`). Field names (`members`, `image`, `name`, `title`, `bio`) are used in the JSON, the PHP, and the tests. - **Scope check:** Single focused build, no decomposition needed. Block is small (≤4 files), testable in isolation, and composable on the Team page (and any other page that needs a team grid). - **Ambiguity check:** The "rounded rectangle" headshot is concretely specified as `rounded-md`. The "open vertically" animation is concretely specified as `grid-template-rows: 0fr → 1fr` with a 300ms transition. The "3-2-1" responsive grid is concretely specified. The "no background" decision is concretely scoped to the Section block wrapper (editor's responsibility, not this block's).