333 lines
15 KiB
Markdown
333 lines
15 KiB
Markdown
# 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 `<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. 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 `<details>`/`<summary>` 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 `<details>` 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
|
|
<?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>
|
|
```
|
|
|
|
Notes:
|
|
- Native `<details>`/`<summary>`: the summary is the click target (photo + name + title); the body is the bio wrap.
|
|
- The `<details>` 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 `<summary>` 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).
|