diff --git a/docs/superpowers/plans/2026-07-30-multi-day-segment-interaction.md b/docs/superpowers/plans/2026-07-30-multi-day-segment-interaction.md new file mode 100644 index 0000000..f2d3de0 --- /dev/null +++ b/docs/superpowers/plans/2026-07-30-multi-day-segment-interaction.md @@ -0,0 +1,670 @@ +# Multi-Day Calendar Segment Interaction 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:** Synchronize the existing hover appearance across every week segment of one continuous event occurrence, with equivalent keyboard-focus behavior and strict isolation between recurrences and calendar instances. + +**Architecture:** PHP assigns each normalized occurrence a key derived from event ID and occurrence index, and every desktop week segment exposes that key in a data attribute. The existing calendar controller groups matching anchors inside each calendar root and maintains independent hover/focus counters, while plugin and MNBC CSS reuse their existing hover colors through one synchronized-state class. + +**Tech Stack:** PHP 8, WordPress, vanilla JavaScript, CSS, DOMDocument/XPath contract tests, Node’s built-in test runner, WordPress Coding Standards. + +--- + +## Repository and File Map + +### AA Events plugin repository + +- Modify `aa-events.php`: release header and runtime asset version `1.2.7`. +- Modify `tests/bootstrap.php`: standalone version fixture. +- Modify `tests/test-admin-consumers.php`: version contract. +- Modify `includes/calendar-layout.php`: canonical occurrence-key helper. +- Modify `templates/calendar.php`: enrich occurrences and emit segment data attributes. +- Modify `tests/test-calendar-layout.php`: occurrence-key and segment-preservation contracts. +- Modify `tests/test-calendar-render.php`: rendered cross-week grouping contract. +- Modify `assets/js/aa-events-calendar-view.js`: occurrence-group interaction controller. +- Modify `tests/test-calendar-view.js`: hover, focus, isolation, and incomplete-markup tests. +- Modify `assets/css/aa-events.css`: synchronized state selector. +- Modify `templates/aa-events.css`: byte-identical stylesheet copy. +- Modify `tests/test-calendar-css.php`: synchronized selector contract. + +### MNBC theme repository + +- Modify `aa-events/calendar.php`: mirror canonical occurrence keys and data attributes. +- Modify `aa-events/aa-events.css`: reuse MNBC’s existing hover colors for synchronized state. +- Create `tests/test-aa-events-segment-interaction.php`: theme override source and CSS contracts. + +## Task 1: Establish the `1.2.7` Version Baseline + +**Files:** + +- Modify: `tests/test-admin-consumers.php` +- Modify: `aa-events.php` +- Modify: `tests/bootstrap.php` + +- [ ] **Step 1: Write the `1.2.7` version expectations** + +In `tests/test-admin-consumers.php`, set both hard-coded release assertions to `1.2.7`: + +```php +aa_events_assert_same( '1.2.7', $header_match[1] ?? '', 'Plugin header uses the feature release version.' ); +aa_events_assert_same( $header_match[1] ?? '', $constant_match[1] ?? '', 'Plugin header and asset version constant match.' ); +aa_events_assert_same( '1.2.7', AA_EVENTS_VERSION, 'Standalone runtime uses the feature release version.' ); +``` + +- [ ] **Step 2: Run the PHP suite and verify RED** + +Run: + +```bash +php tests/run.php +``` + +Expected: version failures showing production `1.2.6` and standalone `1.2.5` instead of `1.2.7`. + +- [ ] **Step 3: Align production and standalone versions** + +In `aa-events.php`: + +```php + * Version: 1.2.7 +``` + +```php +define( 'AA_EVENTS_VERSION', '1.2.7' ); +``` + +In `tests/bootstrap.php`: + +```php +define( 'AA_EVENTS_VERSION', '1.2.7' ); +``` + +- [ ] **Step 4: Run the PHP suite and verify GREEN** + +Run: + +```bash +php tests/run.php +``` + +Expected: `All AA Events tests passed.` + +- [ ] **Step 5: Commit the version alignment** + +```bash +git add aa-events.php tests/bootstrap.php tests/test-admin-consumers.php +git commit -m "chore: prepare AA Events 1.2.7" +``` + +## Task 2: Add Canonical Occurrence Identity to Calendar Segments + +**Files:** + +- Modify: `tests/test-calendar-layout.php` +- Modify: `tests/test-calendar-render.php` +- Modify: `includes/calendar-layout.php` +- Modify: `templates/calendar.php` + +- [ ] **Step 1: Write failing occurrence-key layout contracts** + +In `tests/test-calendar-layout.php`, before the cross-week occurrence fixture, add: + +```php +aa_events_assert_same( '501-0', aa_events_calendar_occurrence_key( 501, 0 ), 'Occurrence key combines event ID and normalized index.' ); +aa_events_assert_same( '501-1', aa_events_calendar_occurrence_key( 501, 1 ), 'Separate recurrences of one event receive distinct keys.' ); +``` + +Add a key to the existing cross-week `$occurrence`: + +```php +'calendar_occurrence_key' => '1-0', +``` + +After its segment assertions, add: + +```php +aa_events_assert_same( + array( '1-0', '1-0' ), + array_column( $segments, 'calendar_occurrence_key' ), + 'Every cross-week segment preserves its occurrence key.' +); +``` + +- [ ] **Step 2: Write the failing rendered-key contract** + +In `tests/test-calendar-render.php`, replace the single event-link lookup with a node-list contract: + +```php +$event_links = $xpath->query( './/a[contains(concat(" ", normalize-space(@class), " "), " aa-events-calendar-event ")]', $grid ); +aa_events_assert_same( 2, $event_links->length, 'Cross-week occurrence renders two event segments.' ); +$event_link = $event_links->item( 0 ); +$occurrence_keys = array(); +foreach ( $event_links as $rendered_event_link ) { + $occurrence_keys[] = $rendered_event_link->getAttribute( 'data-aa-events-occurrence' ); +} +aa_events_assert_same( array( '501-0', '501-0' ), $occurrence_keys, 'Cross-week segments expose one shared occurrence key.' ); +``` + +Retain the existing assertions that use `$event_link`. + +- [ ] **Step 3: Run the PHP suite and verify RED** + +Run: + +```bash +php tests/run.php +``` + +Expected: failure because `aa_events_calendar_occurrence_key()` and the rendered data attributes do not exist. + +- [ ] **Step 4: Implement the canonical key helper** + +Add before `aa_events_calendar_segments()` in `includes/calendar-layout.php`: + +```php +/** + * Build the request-stable key shared by one occurrence's calendar segments. + * + * @param int $post_id Event post ID. + * @param int $occurrence_index Zero-based normalized occurrence index. + * @return string Occurrence key safe for an HTML data attribute. + */ +function aa_events_calendar_occurrence_key( $post_id, $occurrence_index ) { + return (int) $post_id . '-' . (int) $occurrence_index; +} +``` + +- [ ] **Step 5: Enrich occurrences with their key** + +In `templates/calendar.php`, enumerate normalized occurrences: + +```php +foreach ( aa_events_get_occurrences( $event_id ) as $occurrence_index => $occurrence ) { +``` + +Before appending to `$calendar_occurrences`, add: + +```php +$occurrence['calendar_occurrence_key'] = aa_events_calendar_occurrence_key( $event_id, $occurrence_index ); +``` + +- [ ] **Step 6: Emit the occurrence attribute** + +Add this attribute to the desktop segment anchor in `templates/calendar.php`: + +```php +data-aa-events-occurrence="" +``` + +The anchor must retain its classes, inline layout variables, `href`, and `aria-label`. + +- [ ] **Step 7: Run the PHP suite and verify GREEN** + +Run: + +```bash +php tests/run.php +``` + +Expected: `All AA Events tests passed.` + +- [ ] **Step 8: Commit occurrence identity** + +```bash +git add includes/calendar-layout.php templates/calendar.php tests/test-calendar-layout.php tests/test-calendar-render.php +git commit -m "feat: identify continuous calendar segments" +``` + +## Task 3: Synchronize Hover and Focus in the Controller + +**Files:** + +- Modify: `tests/test-calendar-view.js` +- Modify: `assets/js/aa-events-calendar-view.js` + +- [ ] **Step 1: Extend the DOM test double** + +In `tests/test-calendar-view.js`, give `FakeElement` an `allMatches` map, a `querySelectorAll()` method, and a generic dispatcher: + +```js +this.allMatches = {}; +``` + +```js +querySelectorAll(selector) { + return this.allMatches[selector] || []; +} +``` + +```js +dispatch(type) { + for (const listener of this.listeners[type] || []) { + listener({ currentTarget: this }); + } +} +``` + +In `makeCalendar()`, create segment fixtures from `options.segmentKeys`: + +```js +const occurrenceSegments = (options.segmentKeys || []).map((key) => { + const segment = new FakeElement(); + segment.setAttribute('data-aa-events-occurrence', key); + return segment; +}); +root.allMatches['[data-aa-events-occurrence]'] = occurrenceSegments; +``` + +Include `occurrenceSegments` in the returned calendar object. + +- [ ] **Step 2: Write failing synchronized interaction tests** + +Append: + +```js +test('hover synchronizes only matching occurrence segments', () => { + const calendar = makeCalendar({ + segmentKeys: ['501-0', '501-0', '501-1'], + }); + + init(makeDocument([calendar]), makeStorage(), makeMediaQuery()); + calendar.occurrenceSegments[0].dispatch('mouseenter'); + + assert.equal(calendar.occurrenceSegments[0].classList.contains('is-occurrence-active'), true); + assert.equal(calendar.occurrenceSegments[1].classList.contains('is-occurrence-active'), true); + assert.equal(calendar.occurrenceSegments[2].classList.contains('is-occurrence-active'), false); + + calendar.occurrenceSegments[0].dispatch('mouseleave'); + assert.equal(calendar.occurrenceSegments[0].classList.contains('is-occurrence-active'), false); + assert.equal(calendar.occurrenceSegments[1].classList.contains('is-occurrence-active'), false); +}); + +test('hover and keyboard focus keep independent group state', () => { + const calendar = makeCalendar({ segmentKeys: ['501-0', '501-0'] }); + + init(makeDocument([calendar]), makeStorage(), makeMediaQuery()); + calendar.occurrenceSegments[0].dispatch('focus'); + calendar.occurrenceSegments[1].dispatch('mouseenter'); + calendar.occurrenceSegments[0].dispatch('blur'); + + assert.equal(calendar.occurrenceSegments[0].classList.contains('is-occurrence-active'), true); + assert.equal(calendar.occurrenceSegments[1].classList.contains('is-occurrence-active'), true); + + calendar.occurrenceSegments[1].dispatch('mouseleave'); + assert.equal(calendar.occurrenceSegments[0].classList.contains('is-occurrence-active'), false); + assert.equal(calendar.occurrenceSegments[1].classList.contains('is-occurrence-active'), false); +}); + +test('matching keys remain isolated between calendar instances', () => { + const first = makeCalendar({ segmentKeys: ['501-0', '501-0'] }); + const second = makeCalendar({ segmentKeys: ['501-0', '501-0'] }); + + init(makeDocument([first, second]), makeStorage(), makeMediaQuery()); + first.occurrenceSegments[0].dispatch('focus'); + + assert.equal(first.occurrenceSegments[1].classList.contains('is-occurrence-active'), true); + assert.equal(second.occurrenceSegments[0].classList.contains('is-occurrence-active'), false); + assert.equal(second.occurrenceSegments[1].classList.contains('is-occurrence-active'), false); +}); +``` + +Extend the existing incomplete-markup test with segment keys and assert initialization does not throw. + +- [ ] **Step 3: Run the JavaScript tests and verify RED** + +Run: + +```bash +node --test tests/test-calendar-view.js +``` + +Expected: new synchronized-state assertions fail because no occurrence listeners exist. + +- [ ] **Step 4: Implement occurrence-group initialization** + +Add before `getInstance()` in `assets/js/aa-events-calendar-view.js`: + +```js +function initOccurrenceGroups(root) { + var segments = root.querySelectorAll('[data-aa-events-occurrence]'); + var groups = Object.create(null); + + function updateGroup(group) { + var active = group.hoverCount > 0 || group.focusCount > 0; + + for (var memberIndex = 0; memberIndex < group.segments.length; memberIndex += 1) { + group.segments[memberIndex].classList.toggle( + 'is-occurrence-active', + active + ); + } + } + + for (var segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) { + var segment = segments[segmentIndex]; + var key = segment.getAttribute('data-aa-events-occurrence'); + + if (!key) { + continue; + } + + if (!groups[key]) { + groups[key] = { + segments: [], + hoverCount: 0, + focusCount: 0, + }; + } + + groups[key].segments.push(segment); + } + + Object.keys(groups).forEach(function (key) { + var group = groups[key]; + + group.segments.forEach(function (segment) { + var hovered = false; + var focused = false; + + segment.addEventListener('mouseenter', function () { + if (!hovered) { + hovered = true; + group.hoverCount += 1; + updateGroup(group); + } + }); + + segment.addEventListener('mouseleave', function () { + if (hovered) { + hovered = false; + group.hoverCount -= 1; + updateGroup(group); + } + }); + + segment.addEventListener('focus', function () { + if (!focused) { + focused = true; + group.focusCount += 1; + updateGroup(group); + } + }); + + segment.addEventListener('blur', function () { + if (focused) { + focused = false; + group.focusCount -= 1; + updateGroup(group); + } + }); + }); + }); +} +``` + +In the root loop inside `init()`, initialize segment groups before validating view-switcher markup: + +```js +initOccurrenceGroups(roots[index]); +``` + +- [ ] **Step 5: Run JavaScript and PHP suites** + +Run: + +```bash +node --test tests/test-calendar-view.js +php tests/run.php +``` + +Expected: all JavaScript tests and the PHP suite pass. + +- [ ] **Step 6: Commit controller synchronization** + +```bash +git add assets/js/aa-events-calendar-view.js tests/test-calendar-view.js +git commit -m "feat: synchronize calendar segment interaction" +``` + +## Task 4: Reuse Plugin Hover Styling for Active Groups + +**Files:** + +- Modify: `tests/test-calendar-css.php` +- Modify: `assets/css/aa-events.css` +- Modify: `templates/aa-events.css` + +- [ ] **Step 1: Change the CSS contract first** + +In `tests/test-calendar-css.php`, replace the event hover contract with: + +```php +'/\.aa-events-calendar-event:hover\s*,\s*\.aa-events-calendar-event\.is-occurrence-active\s*\{(?=[^}]*background\s*:\s*#163d5d)(?=[^}]*text-decoration\s*:\s*underline)[^}]*\}/s' => 'Hovered and synchronized event segments share the established hover appearance.', +``` + +- [ ] **Step 2: Run the PHP suite and verify RED** + +Run: + +```bash +php tests/run.php +``` + +Expected: synchronized selector contract fails. + +- [ ] **Step 3: Add the synchronized selector** + +In both `assets/css/aa-events.css` and `templates/aa-events.css`, change: + +```css +.aa-events-calendar-event:hover { +``` + +to: + +```css +.aa-events-calendar-event:hover, +.aa-events-calendar-event.is-occurrence-active { +``` + +Do not change the declaration values or the separate `:focus-visible` rule. + +- [ ] **Step 4: Verify CSS behavior and byte identity** + +Run: + +```bash +php tests/run.php +cmp -s assets/css/aa-events.css templates/aa-events.css +``` + +Expected: PHP suite passes and `cmp` exits zero. + +- [ ] **Step 5: Commit plugin styling** + +```bash +git add assets/css/aa-events.css templates/aa-events.css tests/test-calendar-css.php +git commit -m "style: highlight continuous event segments" +``` + +## Task 5: Integrate the MNBC Theme Override + +**Files:** + +- Modify: `/Users/keith/Local Sites/mnbc/app/public/wp-content/themes/MNBC/aa-events/calendar.php` +- Modify: `/Users/keith/Local Sites/mnbc/app/public/wp-content/themes/MNBC/aa-events/aa-events.css` +- Create: `/Users/keith/Local Sites/mnbc/app/public/wp-content/themes/MNBC/tests/test-aa-events-segment-interaction.php` + +- [ ] **Step 1: Work on an isolated theme branch** + +Create an MNBC worktree from current `main` without touching the parent checkout’s existing `views/blocks/section/section.php` change. Use branch: + +```text +keith/multi-day-segment-interaction +``` + +- [ ] **Step 2: Write the failing theme contract** + +Create `tests/test-aa-events-segment-interaction.php`: + +```php +"' ), + 'Calendar override must expose the escaped occurrence key on each segment.' +); +$assert( + 1 === preg_match( + '/\.aa-events-calendar-event:hover\s*,\s*\.aa-events-calendar-event\.is-occurrence-active\s*\{(?=[^}]*background\s*:\s*var\(--color-light-blue\))(?=[^}]*color\s*:\s*var\(--color-dark-blue\))[^}]*\}/s', + $css + ), + 'MNBC synchronized segments must retain the established theme hover colors.' +); + +if ( $failures ) { + exit( 1 ); +} + +echo "All AA Events segment interaction theme contracts passed.\n"; +``` + +- [ ] **Step 3: Run the theme contract and verify RED** + +Run: + +```bash +php tests/test-aa-events-segment-interaction.php +``` + +Expected: key, data attribute, and active selector assertions fail. + +- [ ] **Step 4: Mirror occurrence identity in the theme template** + +In `aa-events/calendar.php`, enumerate occurrences with `$occurrence_index`, set: + +```php +$occurrence['calendar_occurrence_key'] = aa_events_calendar_occurrence_key( $event_id, $occurrence_index ); +``` + +and add to every desktop segment anchor: + +```php +data-aa-events-occurrence="" +``` + +- [ ] **Step 5: Reuse MNBC’s hover appearance** + +In `aa-events/aa-events.css`, change: + +```css +.aa-events-calendar-event:hover { +``` + +to: + +```css +.aa-events-calendar-event:hover, +.aa-events-calendar-event.is-occurrence-active { +``` + +Keep MNBC’s existing light-blue background and dark-blue text. + +- [ ] **Step 6: Run theme verification** + +Run: + +```bash +php tests/test-aa-events-segment-interaction.php +composer lint +git diff --check +``` + +Expected: the segment contract passes, PHPCS completes with zero violations, and no whitespace errors are reported. + +- [ ] **Step 7: Commit the theme integration** + +```bash +git add aa-events/calendar.php aa-events/aa-events.css tests/test-aa-events-segment-interaction.php +git commit -m "feat: synchronize multi-day event segments" +``` + +## Task 6: Full Verification + +**Files:** + +- Verify all plugin and theme changes. + +- [ ] **Step 1: Run plugin behavior suites** + +```bash +php tests/run.php +node --test tests/test-calendar-view.js +``` + +Expected: all PHP and JavaScript tests pass. + +- [ ] **Step 2: Run plugin syntax and focused coding standards** + +```bash +find . -type f -name '*.php' -print0 | xargs -0 -n1 php -l +vendor/bin/phpcs --standard=.phpcs.xml aa-events.php includes/calendar-layout.php templates/calendar.php tests/test-calendar-layout.php tests/test-calendar-render.php tests/test-admin-consumers.php +``` + +If the plugin repository has no `vendor/bin/phpcs`, use the MNBC worktree’s installed `vendor/bin/phpcs` with the plugin `.phpcs.xml`. + +Expected: syntax passes for every plugin PHP file and changed feature files have no PHPCS violations. + +- [ ] **Step 3: Verify plugin source boundaries** + +```bash +cmp -s assets/css/aa-events.css templates/aa-events.css +rg -n "1\\.2\\.[0-9]+|calendar_occurrence_key|data-aa-events-occurrence|is-occurrence-active" aa-events.php includes templates assets tests +git diff --check main..HEAD +git status --short --branch +``` + +Expected: CSS copies are identical; release references are `1.2.7`; occurrence interaction appears only in intended calendar consumers; the feature branch is clean. + +- [ ] **Step 4: Run theme verification** + +From the MNBC feature worktree: + +```bash +php tests/test-aa-events-segment-interaction.php +composer lint +git diff --check main..HEAD +git status --short --branch +``` + +Expected: contracts and lint pass, whitespace is clean, and only intended commits exist. + +- [ ] **Step 5: Review deployment order** + +Confirm the plugin is deployed before or atomically with the theme because the theme template calls `aa_events_calendar_occurrence_key()`. Confirm WordPress caches receive plugin version `1.2.7`, and note that the existing occurrence-index backfill will run in bounded batches after the version change.