📝 docs: Fix brief Task 4 verification grep for CSS nesting

This commit is contained in:
Keith Solomon
2026-07-24 23:34:39 -05:00
parent 8c52946340
commit a13a3adeef
@@ -0,0 +1,614 @@
# Services Content Blocks 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 the missing content-area components for the Mapping services page (and any other service sub-page), matching `Mapping-Services.png`. The hero, header, and footer are already in place and must not be touched.
**Architecture:** One new ACF block (`services-list`) that renders a row of cards. Card count is editor-driven (2, 3, or 4); card size and grid layout adapt to the count. The orange horizontal line that bisects the images is rendered by the block itself as a full-bleed SVG background. The closing "At CWC, mapping is about more than data…" block reuses the existing `pull-quote` block — no new build needed. Editor composes the page in Gutenberg by adding `Services List` and `Pull Quote` blocks to any service sub-page; `page.php` already calls `the_content()`, so no template change is required.
**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 the existing post-title accent pattern (blue heading + orange `::after` underline from `.post-title h1` in `styles/base/misc.css`) for consistency. Do not introduce a new accent style.
- Reuse the existing `pull-quote` block for the closing statement — no duplicate block.
- Page template `page.php` is not modified. The page-children block is not used.
- This work does **not** edit `header.php`, `footer.php`, `views/partials/page-hero.php`, or `views/partials/page-hero-services.php`.
- Tailwind v4 picks up class names from source files at build time. The dynamic `lg:grid-cols-N` class needs to literally appear in the source PHP for Tailwind to emit the rule.
- The `static/img/services-list-line.svg` is a fixed asset; do not make it editor-uploadable.
- All commits follow the project's emoji-prefixed commit-message style (e.g. `✨ feat: …`).
## File Structure
| File | Responsibility | Created/Modified |
| --- | --- | --- |
| `views/blocks/services-list/services-list.php` | Block template (PHP) — renders the row of cards. | Create |
| `views/blocks/services-list/services-list.css` | Block styles (CSS) — layout, headings, bisecting line. | Create |
| `views/blocks/services-list/block.json` | Block registration metadata (name, category, icon, render template). | Create |
| `acf/group_services_list.json` | ACF field group for the `acf/services-list` block. | Create |
| `static/img/services-list-line.svg` | Decorative orange horizontal line SVG that bisects the images. | Create |
| `styles/blocks/index.css` | Import the new block's CSS. | Modify |
| `tests/services-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_services_list.json`
**Interfaces:**
- Produces: ACF group with `key: "group_services_list"`, location scoped to `block == acf/services-list`, fields: `items` (repeater) → `image`, `heading`, `description`.
- [ ] **Step 1: Create the ACF field group JSON file**
Create the file `acf/group_services_list.json` with the following exact content:
```json
{
"key": "group_services_list",
"title": "Services List",
"fields": [
{
"key": "field_services_items",
"label": "Items",
"name": "items",
"aria-label": "",
"type": "repeater",
"instructions": "Add 2, 3, or 4 cards. The grid layout adapts to the count.",
"required": 0,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"layout": "block",
"pagination": 0,
"min": 0,
"max": 0,
"collapsed": "",
"button_label": "Add Service",
"rows_per_page": 20,
"sub_fields": [
{
"key": "field_services_item_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_services_item_heading",
"label": "Heading",
"name": "heading",
"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_services_item_description",
"label": "Description",
"name": "description",
"aria-label": "",
"type": "textarea",
"instructions": "",
"required": 0,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"default_value": "",
"placeholder": "",
"maxlength": "",
"rows": 4,
"new_lines": ""
}
]
}
],
"location": [
[
{
"param": "block",
"operator": "==",
"value": "acf/services-list"
}
]
],
"menu_order": 0,
"position": "normal",
"style": "default",
"label_placement": "top",
"instruction_placement": "label",
"hide_on_screen": "",
"active": true,
"description": "Service cards row. Card count (2-4) determines the layout.",
"show_in_rest": 0,
"display_title": "",
"allow_ai_access": false,
"ai_description": "",
"modified": 1753305600
}
```
- [ ] **Step 2: Verify the JSON parses**
Run: `node -e "JSON.parse(require('fs').readFileSync('acf/group_services_list.json','utf8')); console.log('OK')"`
Expected: prints `OK` and exits 0.
- [ ] **Step 3: Commit**
```bash
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add acf/group_services_list.json
git commit -m "✨ feat: Add ACF field group for services-list block"
```
---
## Task 2: Decorative SVG Asset
**Files:**
- Create: `static/img/services-list-line.svg`
**Interfaces:**
- Produces: A 1600×8 viewBox SVG with a 2px-tall orange `#F26B53` rectangle, `preserveAspectRatio="none"`. Reference URL: `/wp-content/themes/community-works-collaborative/static/img/services-list-line.svg`.
- [ ] **Step 1: Create the SVG asset**
Create the file `static/img/services-list-line.svg` with the following exact content:
```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 8" preserveAspectRatio="none">
<rect x="0" y="3" width="1600" height="2" fill="#F26B53" />
</svg>
```
- [ ] **Step 2: Verify the SVG is well-formed**
Run: `node -e "const fs=require('fs'); const s=fs.readFileSync('static/img/services-list-line.svg','utf8'); if(!s.includes('viewBox=\"0 0 1600 8\"')||!s.includes('fill=\"#F26B53\"')) { process.exit(1); } console.log('OK')"`
Expected: prints `OK` and exits 0.
- [ ] **Step 3: Commit**
```bash
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add static/img/services-list-line.svg
git commit -m "✨ feat: Add services-list decorative line SVG"
```
---
## Task 3: Block Registration JSON
**Files:**
- Create: `views/blocks/services-list/block.json`
**Interfaces:**
- Produces: `block.json` declaring `acf/services-list` as the block name, `services-list.php` as the render template, `grid-view` icon, `common` category. `regACFBlocks()` in `functions.php` will pick this up automatically — no PHP registration code change required.
- [ ] **Step 1: Create the block.json file**
Create the file `views/blocks/services-list/block.json` with the following exact content:
```json
{
"name": "acf/services-list",
"title": "Services List",
"description": "A row of service cards. Card count (2, 3, or 4) determines the layout.",
"category": "common",
"icon": "grid-view",
"keywords": ["services", "cards", "mapping"],
"acf": {
"mode": "preview",
"renderTemplate": "services-list.php"
},
"supports": {
"anchor": true,
"align": false
}
}
```
- [ ] **Step 2: Verify the JSON parses**
Run: `node -e "JSON.parse(require('fs').readFileSync('views/blocks/services-list/block.json','utf8')); console.log('OK')"`
Expected: prints `OK` and exits 0.
- [ ] **Step 3: Commit**
```bash
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add views/blocks/services-list/block.json
git commit -m "✨ feat: Add services-list block.json registration"
```
---
## Task 4: Block CSS
**Files:**
- Create: `views/blocks/services-list/services-list.css`
**Interfaces:**
- Produces: BEM-styled CSS for `.services-list`, `__line`, `__grid`, `__card`, `__media`, `__heading`, `__desc`. Reuses `text-cwc-blue-01` and `var(--color-secondary)` from existing tokens. Renders the bisecting line via `background-image: url('/wp-content/themes/community-works-collaborative/static/img/services-list-line.svg')`.
- [ ] **Step 1: Create the block CSS file**
Create the file `views/blocks/services-list/services-list.css` with the following exact content:
```css
/* Services List block */
.services-list {
--services-line-top: 25%;
&__line {
position: absolute;
inset-inline: 0;
top: var(--services-line-top);
height: 8px;
pointer-events: none;
z-index: 1;
background-image: url('/wp-content/themes/community-works-collaborative/static/img/services-list-line.svg');
background-repeat: no-repeat;
background-position: center center;
background-size: 100% 100%;
}
&__grid {
position: relative;
z-index: 2;
}
&__card {
@apply flex flex-col;
}
&__media {
@apply overflow-hidden rounded-md bg-gray-100;
}
&__img {
@apply w-full h-full object-cover;
}
&__heading {
@apply text-cwc-blue-01 text-25px font-bold leading-none mt-6 mb-4;
&::after {
background: var(--color-secondary);
content: "";
display: block;
height: 4px;
margin-top: 0.45rem;
width: 3rem;
}
}
&__desc {
@apply text-gray-600 text-16px font-light leading-snug;
}
}
@media (max-width: 1023px) {
.services-list__line {
display: none;
}
}
```
- [ ] **Step 2: Verify the CSS contains the expected classes**
The brief uses CSS-nesting shorthand (`&__line`, `&__grid`, etc.) for 6 of the 7 BEM children, and a literal selector (`.services-list__line`) for the one inside the `@media` block. A naive grep for the literal class names will undercount. Use:
```bash
grep -cE "&__(line|grid|card|media|img|heading|desc)|services-list__(line|grid|card|media|img|heading|desc)" views/blocks/services-list/services-list.css
```
Expected: `8` (7 nested `&__` selectors + 1 literal `.services-list__line` inside the `@media` block).
- [ ] **Step 3: Commit**
```bash
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add views/blocks/services-list/services-list.css
git commit -m "✨ feat: Add services-list block CSS"
```
---
## Task 5: Block Template (PHP)
**Files:**
- Create: `views/blocks/services-list/services-list.php`
**Interfaces:**
- Consumes: `get_field( 'items' )` → array of items each with `image` (array: `url`, `alt`), `heading` (string), `description` (string).
- Produces: A `<section class="services-list ...">` element containing the bisecting `__line` div, a `container` with a grid div, and a `<article class="services-list__card">` per item. Computes the dynamic `lg:grid-cols-N` class from item count.
- [ ] **Step 1: Create the block PHP template**
Create the file `views/blocks/services-list/services-list.php` with the following exact content (tabs for indentation):
```php
<?php
/**
* Block Name: Services List
*
* A row of service cards. Card count (2, 3, or 4) determines the layout.
*
* @package CWC
*/
namespace CWC;
$items = get_field( 'items' );
if ( empty( $items ) ) {
return;
}
$count = count( $items );
$grid_cols = max( 2, min( 4, (int) $count ) );
$grid_class = 'lg:grid-cols-' . $grid_cols;
$line_url = '/wp-content/themes/community-works-collaborative/static/img/services-list-line.svg';
$classes = 'services-list mx-break-out relative ' . $grid_class;
$wrapper = blockWrapperAttributes( $classes, $is_preview );
?>
<section <?php echo wp_kses_post( $wrapper ); ?>>
<div class="services-list__line" aria-hidden="true" style="background-image: url('<?php echo esc_url( $line_url ); ?>');"></div>
<div class="container">
<div class="services-list__grid grid grid-cols-1 <?php echo esc_attr( $grid_class ); ?> gap-8 lg:gap-12">
<?php foreach ( $items as $item ) : ?>
<article class="services-list__card">
<?php if ( ! empty( $item['image'] ) ) : ?>
<figure class="services-list__media aspect-square">
<img
class="services-list__img"
src="<?php echo esc_url( $item['image']['url'] ); ?>"
alt="<?php echo esc_attr( $item['image']['alt'] ?? '' ); ?>"
loading="lazy"
/>
</figure>
<?php endif; ?>
<?php if ( ! empty( $item['heading'] ) ) : ?>
<h3 class="services-list__heading">
<?php echo wp_kses_post( $item['heading'] ); ?>
</h3>
<?php endif; ?>
<?php if ( ! empty( $item['description'] ) ) : ?>
<div class="services-list__desc">
<?php echo wp_kses_post( $item['description'] ); ?>
</div>
<?php endif; ?>
</article>
<?php endforeach; ?>
</div>
</div>
</section>
```
- [ ] **Step 2: Lint the PHP file with PHPCS**
Run: `composer exec phpcs -- views/blocks/services-list/services-list.php`
Expected: exits 0 with no errors. If there are pre-existing project-wide issues unrelated to this file, fix only the ones in this file.
- [ ] **Step 3: Verify the dynamic grid class is in the source**
Run: `grep -E "lg:grid-cols-\$grid_cols|lg:grid-cols-' \. \$grid_cols" views/blocks/services-list/services-list.php`
Expected: at least one match (Tailwind must see the literal class name in the source to emit the rule).
- [ ] **Step 4: Commit**
```bash
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add views/blocks/services-list/services-list.php
git commit -m "✨ feat: Add services-list block PHP template"
```
---
## Task 6: CSS Import Wiring
**Files:**
- Modify: `styles/blocks/index.css`
**Interfaces:**
- Consumes: `views/blocks/services-list/services-list.css` (from Task 4).
- 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`:
```css
@import '../../views/blocks/services-list/services-list.css';
```
After the edit, the file should end with:
```
@import '../../views/blocks/contact-info/contact-info.css';
@import '../../views/blocks/services-list/services-list.css';
```
- [ ] **Step 2: Verify the import is present**
Run: `grep -c "services-list/services-list.css" styles/blocks/index.css`
Expected: `1`.
- [ ] **Step 3: Commit**
```bash
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 services-list block CSS into theme"
```
---
## Task 7: Build the CSS
**Files:**
- Modify: `static/dist/theme.css`
**Interfaces:**
- Consumes: The new import in `styles/blocks/index.css` (from Task 6) and the block CSS (from Task 4).
- Produces: A rebuilt `static/dist/theme.css` that includes the `.services-list*` rules and the dynamic `lg:grid-cols-2`, `lg:grid-cols-3`, `lg:grid-cols-4` utilities.
- [ ] **Step 1: Run the production Tailwind build**
Run: `npm run build`
Expected: completes with no errors. The output file `static/dist/theme.css` is updated in place.
- [ ] **Step 2: Verify the services-list CSS classes are in the built output**
Run: `grep -c "services-list__heading" static/dist/theme.css`
Expected: `1` or more (the class should appear in the built CSS).
- [ ] **Step 3: Verify the dynamic grid classes are in the built output**
Run: `grep -E "lg\\\\:grid-cols-(2|3|4) " static/dist/theme.css | wc -l`
Expected: `3` (all three column counts should be present).
- [ ] **Step 4: Commit the rebuilt dist**
```bash
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 services-list block styles"
```
---
## Task 8: Playwright Tests
**Files:**
- Create: `tests/services-page.spec.js`
**Interfaces:**
- Consumes: The rendered `views/blocks/services-list/services-list.php` template (Tasks 4 + 5) on the WordPress page `/services/mapping/`.
- Produces: Three tests:
1. Card count, image, and heading visibility per block (expects 2 + 4 cards across two block instances).
2. Bisecting `__line` is present and `aria-hidden="true"`.
3. No axe-core a11y violations scoped to `.services-list`.
> **Prerequisite:** the test depends on the mapping page being composed in Gutenberg with two `Services List` blocks (2 items + 4 items) and a `Pull Quote` block. 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/services-page.spec.js` with the following exact content:
```js
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test.describe("Services List block on /services/mapping/", () => {
test("renders the heading and image for each card", async ({ page }) => {
await page.goto("/services/mapping/");
const blocks = page.locator(".services-list");
await expect(blocks).toHaveCount(2);
await expect(blocks.nth(0).locator(".services-list__card")).toHaveCount(2);
await expect(blocks.nth(1).locator(".services-list__card")).toHaveCount(4);
const firstCard = blocks.nth(0).locator(".services-list__card").first();
await expect(firstCard.locator(".services-list__media img")).toBeVisible();
await expect(firstCard.locator(".services-list__heading")).toBeVisible();
});
test("bisecting line is present and aria-hidden", async ({ page }) => {
await page.goto("/services/mapping/");
const line = page.locator(".services-list__line").first();
await expect(line).toBeAttached();
await expect(line).toHaveAttribute("aria-hidden", "true");
});
test("no axe violations on the services-list block", async ({ page }) => {
await page.goto("/services/mapping/");
const results = await new AxeBuilder({ page })
.include(".services-list")
.analyze();
expect(results.violations).toEqual([]);
});
});
```
- [ ] **Step 2: Run the tests**
Run: `npx playwright test tests/services-page.spec.js`
Expected: all 3 tests pass. If tests fail because the mapping 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**
```bash
cd "C:/Users/ksolo/Herd/community-works-collaborative/wp-content/themes/community-works-collaborative"
git add tests/services-page.spec.js
git commit -m "✅ test: Add Playwright tests for services-list block"
```
---
## Self-Review Notes
- **Spec coverage:**
- One new ACF block (`services-list`) — covered by Tasks 1, 3, 4, 5.
- Repeater of `items` with `image` / `heading` / `description` — covered by Task 1.
- Card count drives grid columns (2/3/4) — covered by Task 5 (PHP computes the class) and Task 7 (Tailwind emits the rule).
- Orange bisecting line, full-bleed, behind the cards, hidden on mobile — covered by Tasks 2 (SVG), 4 (CSS with `inset-inline: 0`, `z-index: 1` on line, `z-index: 2` on grid, media query to hide on `<lg`).
- Reuse `pull-quote` for closing — explicitly out of scope (no code change needed).
- 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`, `var(--color-secondary)`, brand orange `#F26B53`) — confirmed in Tasks 4 and 2.
- Playwright tests covering count, image, heading, line, a11y — covered by Task 8.
- Dist rebuild — covered by Task 7.
- **Placeholder scan:** No TBDs. Every step has a concrete file path, exact content, exact command, and expected output. The only conditional is in Task 8 Step 2 — explicitly called out as a prerequisite (editor must compose the page in Gutenberg), not a placeholder.
- **Type consistency:** ACF field `name: "items"` is used in Task 5 (`get_field( 'items' )`). Sub-field names `image`, `heading`, `description` are used in Task 1 (defined) and Task 5 (consumed). Class names `services-list`, `__line`, `__grid`, `__card`, `__media`, `__img`, `__heading`, `__desc` are used in Task 4 (defined in CSS) and Task 5 (rendered in markup) and Task 8 (asserted in tests) — consistent.
- **Ambiguity check:** "Editor adds 2 instances of the block on the mapping page" is the chosen approach (per spec §1 and design Q&A). "Bisecting line" is a single orange SVG, 8px tall, positioned at `top: 25%` of the block wrapper, hidden on mobile. Both are explicit.