diff --git a/docs/superpowers/plans/2026-07-24-services-content-blocks.md b/docs/superpowers/plans/2026-07-24-services-content-blocks.md new file mode 100644 index 0000000..a69119c --- /dev/null +++ b/docs/superpowers/plans/2026-07-24-services-content-blocks.md @@ -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 + + + +``` + +- [ ] **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 `
` element containing the bisecting `__line` div, a `container` with a grid div, and a `
` 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 echo esc_attr( $item['image']['alt'] ?? '' ); ?> +
+ + + +

+ +

+ + + +
+ +
+ +
+ +
+
+
+``` + +- [ ] **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 `