From 619409eabb2587fa1a3a9ec6b1adc84e2e20e9d8 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sat, 25 Jul 2026 12:46:27 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Add=20design=20spec=20fo?= =?UTF-8?q?r=20team=20grid=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-07-25-team-grid-block-design.md | 332 ++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-25-team-grid-block-design.md diff --git a/docs/superpowers/specs/2026-07-25-team-grid-block-design.md b/docs/superpowers/specs/2026-07-25-team-grid-block-design.md new file mode 100644 index 0000000..c724b72 --- /dev/null +++ b/docs/superpowers/specs/2026-07-25-team-grid-block-design.md @@ -0,0 +1,332 @@ +# 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).