- Introduced a new specification for adding an optional contact email field to events, including front-end output and compatibility testing. - Added a comprehensive design document for multi-day event occurrences, detailing the new occurrence data model, validation, normalization, and calendar architecture. - Created specifications for ACF field-key switching compatibility and local ACF override compatibility to ensure seamless integration with existing themes. - Implemented a desktop calendar view toggle design, allowing users to switch between calendar and list views, with persistent preferences and responsive behavior.
33 KiB
Multi-Day Event Occurrences 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: Support mixed recurring and multi-day event occurrences, render multi-day bars across the traditional month grid, and provide a chronological mobile agenda.
Architecture: Keep event_dates as the persisted ACF repeater and normalize all supported field formats through a focused occurrence API. Put date normalization/formatting in includes/occurrences.php and pure calendar segmentation/lane logic in includes/calendar-layout.php; templates consume those APIs without interpreting raw ACF rows. Desktop rendering uses week containers with a seven-column CSS grid, while mobile renders the same normalized occurrences as an agenda.
Tech Stack: PHP 7.4+, WordPress APIs, Advanced Custom Fields, semantic HTML, CSS Grid, dependency-free PHP test runner
File Structure
- Create
includes/occurrences.php: normalize ACF/legacy values, validate ranges, format occurrence labels, and migrate legacy values on save. - Create
includes/calendar-layout.php: calculate month bounds, split occurrences into weekly segments, and assign overlap lanes. - Create
tests/bootstrap.php: minimal WordPress/ACF stubs and assertion helpers for dependency-free unit tests. - Create
tests/test-occurrences.php: normalization, formatting, validation, and migration tests. - Create
tests/test-calendar-layout.php: month intersection, week splitting, and lane-allocation tests. - Create
tests/run.php: execute all test files and return a nonzero status on failure. - Modify
includes/class-aa-events.php: load the two focused helper modules before post type/template code. - Modify
includes/acf-fields.php: extend and relabel the repeater, register validation, and make the legacy field less prominent. - Modify
assets/js/aa-events-admin.js: clear/disable time inputs while an occurrence is marked all-day. - Modify
includes/post-types.php: replace raw/mismatched repeater reads with normalized occurrences and update admin date sorting. - Modify
templates/single-event.php: render formatted occurrence ranges. - Modify
templates/archive-event.php: render formatted occurrence ranges. - Rewrite
templates/calendar.php: collect intersecting occurrences and render desktop week grids plus mobile agenda markup. - Modify
assets/css/aa-events.css: style the week grid, spanning bars, continuations, focus states, and mobile agenda. - Modify
templates/aa-events.css: keep the documented theme-copy stylesheet synchronized with the bundled stylesheet. - Modify
README.md: document occurrence fields, compatibility, responsive behavior, and theme-override implications.
The current directory is not a Git worktree. Commit steps below are required once this plugin is placed in a repository; until then, record each checkpoint with test output and skip only the git add/git commit commands.
Task 1: Add a Dependency-Free PHP Test Harness
Files:
-
Create:
tests/bootstrap.php -
Create:
tests/run.php -
Step 1: Create WordPress and ACF test doubles
Create tests/bootstrap.php with mutable field storage and strict assertion helpers:
<?php
define( 'ABSPATH', __DIR__ . '/' );
$GLOBALS['aa_events_test_fields'] = array();
$GLOBALS['aa_events_test_updates'] = array();
$GLOBALS['aa_events_test_failures'] = 0;
function get_field( $name, $post_id = false ) {
return $GLOBALS['aa_events_test_fields'][ (int) $post_id ][ $name ] ?? null;
}
function update_field( $name, $value, $post_id = false ) {
$GLOBALS['aa_events_test_fields'][ (int) $post_id ][ $name ] = $value;
$GLOBALS['aa_events_test_updates'][] = array( $name, $value, (int) $post_id );
return true;
}
function wp_timezone() {
return new DateTimeZone( 'America/Winnipeg' );
}
function wp_date( $format, $timestamp = null, $timezone = null ) {
$date = new DateTimeImmutable( '@' . ( null === $timestamp ? time() : $timestamp ) );
return $date->setTimezone( $timezone ?: wp_timezone() )->format( $format );
}
function __( $text ) { return $text; }
function _x( $text ) { return $text; }
function aa_events_test_reset() {
$GLOBALS['aa_events_test_fields'] = array();
$GLOBALS['aa_events_test_updates'] = array();
}
function aa_events_assert_same( $expected, $actual, $message ) {
if ( $expected !== $actual ) {
++$GLOBALS['aa_events_test_failures'];
fwrite( STDERR, "FAIL: {$message}\nExpected: " . var_export( $expected, true ) . "\nActual: " . var_export( $actual, true ) . "\n" );
}
}
function aa_events_assert_true( $actual, $message ) {
aa_events_assert_same( true, (bool) $actual, $message );
}
- Step 2: Create the test entry point
Create tests/run.php:
<?php
require __DIR__ . '/bootstrap.php';
require dirname( __DIR__ ) . '/includes/occurrences.php';
require dirname( __DIR__ ) . '/includes/calendar-layout.php';
require __DIR__ . '/test-occurrences.php';
require __DIR__ . '/test-calendar-layout.php';
if ( $GLOBALS['aa_events_test_failures'] > 0 ) {
fwrite( STDERR, $GLOBALS['aa_events_test_failures'] . " test(s) failed.\n" );
exit( 1 );
}
fwrite( STDOUT, "All AA Events tests passed.\n" );
- Step 3: Run the empty harness
Run: php tests/run.php
Expected: FAIL because includes/occurrences.php does not exist. This confirms the test command detects missing production code.
- Step 4: Commit the harness when Git is available
git add tests/bootstrap.php tests/run.php
git commit -m "test: add lightweight event helper harness"
Task 2: Normalize and Format Occurrences
Files:
-
Create:
tests/test-occurrences.php -
Create:
includes/occurrences.php -
Modify:
includes/class-aa-events.php -
Step 1: Write failing normalization tests
Create tests/test-occurrences.php with these cases:
<?php
aa_events_test_reset();
$GLOBALS['aa_events_test_fields'][10]['event_dates'] = array(
array(
'date' => '2026-07-10',
'start_time' => '18:00:00',
'end_date' => '2026-07-12',
'end_time' => '14:00:00',
'all_day' => false,
),
array(
'date' => '2026-07-20',
'start_time' => '',
'end_date' => '',
'end_time' => '',
'all_day' => true,
),
);
$occurrences = aa_events_get_occurrences( 10 );
aa_events_assert_same( 2, count( $occurrences ), 'Repeater rows normalize.' );
aa_events_assert_same( '2026-07-12', $occurrences[0]['end_date'], 'Explicit end date is retained.' );
aa_events_assert_same( '2026-07-20', $occurrences[1]['end_date'], 'Missing end date defaults to start date.' );
aa_events_assert_true( $occurrences[1]['all_day'], 'All-day flag normalizes to boolean.' );
aa_events_test_reset();
$GLOBALS['aa_events_test_fields'][11]['event_datetime'] = '2026-08-03 09:30:00';
$legacy = aa_events_get_occurrences( 11 );
aa_events_assert_same( '2026-08-03', $legacy[0]['start_date'], 'Legacy datetime supplies start date.' );
aa_events_assert_same( '09:30:00', $legacy[0]['start_time'], 'Legacy datetime supplies start time.' );
aa_events_assert_same( 'event_datetime', $legacy[0]['source'], 'Legacy source is identified.' );
aa_events_test_reset();
$GLOBALS['aa_events_test_fields'][12]['date'] = '2026-09-01';
$GLOBALS['aa_events_test_fields'][12]['start_time'] = '11:00:00';
$separate = aa_events_get_occurrences( 12 );
aa_events_assert_same( 'legacy_separate', $separate[0]['source'], 'Separate legacy fields normalize.' );
aa_events_test_reset();
$GLOBALS['aa_events_test_fields'][13]['event_dates'] = array(
array( 'date' => '', 'end_date' => '2026-07-02' ),
array( 'date' => 'not-a-date' ),
);
aa_events_assert_same( array(), aa_events_get_occurrences( 13 ), 'Malformed rows are skipped.' );
- Step 2: Add failing validation and formatting tests
Append to tests/test-occurrences.php:
aa_events_assert_same(
'End date cannot be earlier than start date.',
aa_events_validate_occurrence_row(
array( 'date' => '2026-07-12', 'end_date' => '2026-07-10' )
),
'Backwards date range is rejected.'
);
aa_events_assert_same(
'End time cannot be earlier than start time on the same date.',
aa_events_validate_occurrence_row(
array(
'date' => '2026-07-10',
'start_time' => '18:00:00',
'end_time' => '14:00:00',
)
),
'Backwards same-day time range is rejected.'
);
aa_events_assert_same(
'',
aa_events_validate_occurrence_row(
array( 'date' => '2026-07-10', 'end_date' => '2026-07-12', 'all_day' => true )
),
'Valid all-day range passes.'
);
$timed = aa_events_normalize_occurrence_row(
array(
'date' => '2026-07-10',
'start_time' => '18:00:00',
'end_date' => '2026-07-12',
'end_time' => '14:00:00',
),
'event_dates'
);
aa_events_assert_same(
'July 10, 2026, 6:00 PM–July 12, 2026, 2:00 PM',
aa_events_format_occurrence( $timed ),
'Timed multi-day range formats both endpoints.'
);
$all_day = aa_events_normalize_occurrence_row(
array( 'date' => '2026-07-10', 'end_date' => '2026-07-12', 'all_day' => true ),
'event_dates'
);
aa_events_assert_same(
'July 10–12, 2026 — All day',
aa_events_format_occurrence( $all_day ),
'All-day range uses compact localized dates.'
);
- Step 3: Run tests to verify they fail
Run: php tests/run.php
Expected: FAIL with undefined occurrence function errors.
- Step 4: Implement the occurrence API
Create includes/occurrences.php. Implement these exact public functions:
function aa_events_normalize_occurrence_row( array $row, $source = 'event_dates', $post_id = 0 )
function aa_events_get_occurrences( $post_id )
function aa_events_validate_occurrence_row( array $row )
function aa_events_format_occurrence( array $occurrence )
function aa_events_occurrence_start( array $occurrence )
function aa_events_occurrence_end( array $occurrence )
function aa_events_migrate_legacy_occurrence( $post_id )
aa_events_normalize_occurrence_row() must return null for invalid start dates and otherwise return this stable shape:
array(
'post_id' => (int) $post_id,
'start_date' => 'Y-m-d',
'start_time' => 'H:i:s or empty string',
'end_date' => 'Y-m-d',
'end_time' => 'H:i:s or empty string',
'all_day' => (bool) $all_day,
'source' => 'event_dates|event_datetime|legacy_separate',
)
Use strict DateTimeImmutable::createFromFormat() parsing in wp_timezone(), check DateTimeImmutable::getLastErrors() when it returns an array, ignore times when all_day is true, and sort normalized occurrences by aa_events_occurrence_start().
aa_events_format_occurrence() must use wp_date() for every visible date/time and cover the five formats specified in the design. Use an en dash between endpoints and an em dash before “All day.”
- Step 5: Load occurrence helpers before consumers
In AA_Events::includes(), add this as the first include:
include_once AA_EVENTS_PLUGIN_DIR . 'includes/occurrences.php';
- Step 6: Run occurrence tests
Run: php tests/run.php
Expected: occurrence assertions pass; execution then fails because calendar-layout files do not yet exist.
- Step 7: Commit the normalized occurrence API when Git is available
git add includes/occurrences.php includes/class-aa-events.php tests/test-occurrences.php
git commit -m "feat: normalize event occurrences"
Task 3: Extend the ACF Editor, Validate Rows, and Migrate Legacy Data
Files:
-
Modify:
includes/acf-fields.php -
Modify:
includes/occurrences.php -
Modify:
assets/js/aa-events-admin.js -
Modify:
tests/test-occurrences.php -
Step 1: Write failing migration tests
Append to tests/test-occurrences.php:
aa_events_test_reset();
$GLOBALS['aa_events_test_fields'][20]['event_datetime'] = '2026-10-05 13:15:00';
aa_events_assert_true( aa_events_migrate_legacy_occurrence( 20 ), 'Legacy value migrates.' );
aa_events_assert_same(
array(
array(
'date' => '2026-10-05',
'start_time' => '13:15:00',
'end_date' => '',
'end_time' => '',
'all_day' => 0,
),
),
$GLOBALS['aa_events_test_fields'][20]['event_dates'],
'Migration writes one compatible repeater row.'
);
aa_events_assert_same( false, aa_events_migrate_legacy_occurrence( 20 ), 'Second migration is idempotent.' );
aa_events_assert_same( 1, count( $GLOBALS['aa_events_test_updates'] ), 'Second migration writes nothing.' );
- Step 2: Run migration test to verify it fails
Run: php tests/run.php
Expected: FAIL because migration is not yet implemented or does not write the required row shape.
- Step 3: Implement idempotent migration and save hook
Complete aa_events_migrate_legacy_occurrence() so it returns false when event_dates already has rows or no parseable legacy value exists. Otherwise, call update_field( 'event_dates', array( $row ), $post_id ) and return its boolean result.
Register a save callback that ignores autosaves/revisions and non-event posts:
function aa_events_migrate_legacy_on_save( $post_id ) {
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( 'event' !== get_post_type( $post_id ) ) {
return;
}
aa_events_migrate_legacy_occurrence( $post_id );
}
add_action( 'acf/save_post', 'aa_events_migrate_legacy_on_save', 20 );
- Step 4: Replace the built-in repeater definition
In includes/acf-fields.php, retain field key field_61b0c7f0a3e8f and name event_dates, but change its label/button/instructions to:
'label' => 'Occurrences',
'instructions' => 'Each row is one occurrence. Add an end date for a continuous multi-day event; add more rows when the event recurs.',
'layout' => 'row',
'button_label' => 'Add Occurrence',
Keep date and start_time, then add subfields with unique stable keys:
array(
'key' => 'field_aa_events_end_date',
'label' => 'End Date',
'name' => 'end_date',
'type' => 'date_picker',
'required' => 0,
'display_format'=> 'F j, Y',
'return_format' => 'Y-m-d',
'first_day' => 1,
),
array(
'key' => 'field_aa_events_end_time',
'label' => 'End Time',
'name' => 'end_time',
'type' => 'time_picker',
'required' => 0,
'display_format'=> 'g:i a',
'return_format' => 'H:i:s',
),
array(
'key' => 'field_aa_events_all_day',
'label' => 'All Day',
'name' => 'all_day',
'type' => 'true_false',
'ui' => 1,
'default_value' => 0,
),
Change the legacy event_datetime instructions to “Legacy compatibility field. Use Occurrences for new events.”
- Step 5: Register row validation through ACF
Add acf/validate_value/key=field_61b0c7f0a3e8f validation that loops submitted rows, maps ACF subfield keys to semantic names, calls aa_events_validate_occurrence_row(), and returns a translated row-numbered error such as Occurrence 2: End date cannot be earlier than start date. Return the incoming $valid unchanged when it is already an error.
- Step 6: Implement all-day editor behavior
In assets/js/aa-events-admin.js, add a delegated change handler scoped to .acf-field[data-name="event_dates"]. For each repeater row, toggle an aa-events-is-all-day class, disable the start_time and end_time inputs while checked, and clear those inputs when the checkbox changes from unchecked to checked. Run this synchronization on ACF ready and append actions.
- Step 7: Run tests and PHP syntax checks
Run:
php tests/run.php
php -l includes/acf-fields.php
php -l includes/occurrences.php
Expected: all occurrence tests pass and both files report No syntax errors detected.
- Step 8: Commit editor and migration behavior when Git is available
git add includes/acf-fields.php includes/occurrences.php assets/js/aa-events-admin.js tests/test-occurrences.php
git commit -m "feat: add occurrence ranges to event editor"
Task 4: Build and Test Calendar Segmentation
Files:
-
Create:
tests/test-calendar-layout.php -
Create:
includes/calendar-layout.php -
Modify:
includes/class-aa-events.php -
Step 1: Write failing week-segment tests
Create tests/test-calendar-layout.php:
<?php
$month = aa_events_calendar_month( 2026, 7, 1 );
aa_events_assert_same( '2026-06-29', $month['grid_start']->format( 'Y-m-d' ), 'Monday-start grid begins before month.' );
aa_events_assert_same( '2026-08-02', $month['grid_end']->format( 'Y-m-d' ), 'Grid ends after complete final week.' );
$occurrence = aa_events_normalize_occurrence_row(
array(
'date' => '2026-07-10',
'start_time' => '18:00:00',
'end_date' => '2026-07-15',
'end_time' => '14:00:00',
),
'event_dates',
30
);
$segments = aa_events_calendar_segments( $occurrence, $month );
aa_events_assert_same( 2, count( $segments ), 'Occurrence splits at week boundary.' );
aa_events_assert_same( 4, $segments[0]['start_column'], 'Friday is column four in Monday-start grid.' );
aa_events_assert_same( 3, $segments[0]['span'], 'First segment covers Friday through Sunday.' );
aa_events_assert_true( $segments[0]['continues_after'], 'First segment continues next week.' );
aa_events_assert_same( 0, $segments[1]['start_column'], 'Second segment starts Monday.' );
aa_events_assert_same( 3, $segments[1]['span'], 'Second segment covers Monday through Wednesday.' );
aa_events_assert_true( $segments[1]['continues_before'], 'Second segment continued from prior week.' );
- Step 2: Add failing clipping and lane tests
Append:
$cross_month = aa_events_normalize_occurrence_row(
array( 'date' => '2026-06-25', 'end_date' => '2026-07-03', 'all_day' => true ),
'event_dates',
31
);
$clipped = aa_events_calendar_segments( $cross_month, $month );
aa_events_assert_true( $clipped[0]['continues_before'], 'Month-clipped segment shows prior continuation.' );
$week_segments = array(
array( 'start_column' => 0, 'span' => 3, 'post_id' => 1 ),
array( 'start_column' => 2, 'span' => 2, 'post_id' => 2 ),
array( 'start_column' => 4, 'span' => 2, 'post_id' => 3 ),
);
$lanes = aa_events_assign_segment_lanes( $week_segments );
aa_events_assert_same( 0, $lanes[0]['lane'], 'First segment uses first lane.' );
aa_events_assert_same( 1, $lanes[1]['lane'], 'Overlapping segment uses second lane.' );
aa_events_assert_same( 0, $lanes[2]['lane'], 'Non-overlapping segment reuses first lane.' );
- Step 3: Run tests to verify failure
Run: php tests/run.php
Expected: FAIL with undefined calendar-layout functions.
- Step 4: Implement pure layout functions
Create includes/calendar-layout.php with these public functions:
function aa_events_calendar_month( $year, $month, $start_of_week )
function aa_events_occurrence_intersects_range( array $occurrence, DateTimeImmutable $start, DateTimeImmutable $end )
function aa_events_calendar_segments( array $occurrence, array $month )
function aa_events_assign_segment_lanes( array $segments )
function aa_events_group_calendar_weeks( array $occurrences, array $month )
aa_events_calendar_month() must return month_start, month_end, grid_start, grid_end, start_of_week, and an ordered weeks array. Each week contains start, end, and seven days.
aa_events_calendar_segments() must clip to month_start/month_end, split at each configured week boundary, use zero-based columns, and set continues_before/continues_after relative to the true occurrence interval.
aa_events_assign_segment_lanes() must sort by start column, then longest span, then post ID. Assign the first lane whose occupied end column is strictly before the segment start column.
aa_events_group_calendar_weeks() must return each week with lane-assigned segments and lane_count, preserving complete occurrence data on every segment for template labels.
- Step 5: Load the layout helper
In AA_Events::includes(), immediately after the occurrence include, add:
include_once AA_EVENTS_PLUGIN_DIR . 'includes/calendar-layout.php';
- Step 6: Run layout tests
Run: php tests/run.php
Expected: All AA Events tests passed.
- Step 7: Commit layout logic when Git is available
git add includes/calendar-layout.php includes/class-aa-events.php tests/test-calendar-layout.php
git commit -m "feat: calculate multi-day calendar segments"
Task 5: Move All Existing Consumers to the Occurrence API
Files:
-
Modify:
includes/post-types.php -
Modify:
templates/single-event.php -
Modify:
templates/archive-event.php -
Step 1: Replace the compatibility helper
Keep events_get_all_event_dates() as a deprecated compatibility wrapper so theme overrides do not fatally break:
function events_get_all_event_dates( $post_id ) {
$dates = array();
foreach ( aa_events_get_occurrences( $post_id ) as $occurrence ) {
$dates[] = array(
'date' => $occurrence['start_date'],
'time' => $occurrence['start_time'],
'end_date' => $occurrence['end_date'],
'end_time' => $occurrence['end_time'],
'all_day' => $occurrence['all_day'],
);
}
return $dates;
}
Remove direct reads of the nonexistent/mismatched multiple_dates field from admin display code.
- Step 2: Update the admin column display
Call aa_events_get_occurrences( $post_id ), display aa_events_format_occurrence( $occurrences[0] ), and retain the existing +N badge with escaped count. If no normalized occurrence exists, render an em dash.
- Step 3: Correct admin SQL sorting keys
Change the first repeater date join from multiple_dates_0_date to event_dates_0_date. Keep the COALESCE fallback order event_dates_0_date, event_datetime, date, post date. Add $wpdb->posts.ID as a final deterministic ordering key.
- Step 4: Update archive and single templates
In both templates, replace local timestamp construction with:
$occurrences = aa_events_get_occurrences( get_the_ID() );
foreach ( $occurrences as $index => $occurrence ) {
$start = aa_events_occurrence_start( $occurrence );
printf(
'<time datetime="%1$s">%2$s</time>%3$s',
esc_attr( $start->format( DATE_W3C ) ),
esc_html( aa_events_format_occurrence( $occurrence ) ),
$index < count( $occurrences ) - 1 ? '<br />' : ''
);
}
Change the visible label from “Date:” to the plural-aware Dates: only when more than one occurrence exists.
- Step 5: Run tests and syntax checks
Run:
php tests/run.php
php -l includes/post-types.php
php -l templates/single-event.php
php -l templates/archive-event.php
Expected: tests pass and every PHP file reports no syntax errors.
- Step 6: Commit consumer updates when Git is available
git add includes/post-types.php templates/single-event.php templates/archive-event.php
git commit -m "refactor: render normalized event occurrences"
Task 6: Render the Desktop Month Grid and Mobile Agenda
Files:
-
Modify:
templates/calendar.php -
Step 1: Replace per-day event collection
Retain validated navigation parameters and localized weekday construction. Replace $events_by_day with $calendar_occurrences. For each published event, append every normalized occurrence that intersects $month['month_start'] through $month['month_end'], adding title, permalink, and post_id to the normalized array.
Sort the mobile list with start timestamp, then title:
usort(
$calendar_occurrences,
function ( $a, $b ) {
$comparison = aa_events_occurrence_start( $a ) <=> aa_events_occurrence_start( $b );
return 0 !== $comparison ? $comparison : strcasecmp( $a['title'], $b['title'] );
}
);
Pass the occurrences into aa_events_group_calendar_weeks() for desktop rendering.
- Step 2: Render semantic desktop week grids
Replace the event-bearing table body with .aa-events-calendar-grid, while retaining a visually aligned weekday header. For each week render:
<div class="aa-events-calendar-week" role="row" style="--aa-event-lanes: <?php echo esc_attr( max( 1, $week['lane_count'] ) ); ?>">
<div class="aa-events-calendar-days">
<?php foreach ( $week['days'] as $day ) : ?>
<div role="gridcell" class="aa-events-calendar-day ..." aria-label="...">
<span class="day-number"><?php echo esc_html( $day->format( 'j' ) ); ?></span>
</div>
<?php endforeach; ?>
</div>
<div class="aa-events-calendar-bars">
<!-- segment links -->
</div>
</div>
Render every segment link with CSS variables --aa-event-column, --aa-event-span, and --aa-event-lane. Add is-continuing-before and is-continuing-after classes. Repeat the visible title on every segment and build aria-label from title plus the complete aa_events_format_occurrence() result. Mark visible arrows aria-hidden="true".
Days outside the selected month remain visible as muted grid cells so week geometry stays intact. Keep the current-day class based on site timezone.
- Step 3: Render mobile agenda markup
After the desktop grid, render .aa-events-calendar-agenda with a heading and <ol>. Each intersecting occurrence appears once:
<li class="aa-events-agenda-item">
<a href="<?php echo esc_url( $occurrence['permalink'] ); ?>">
<span class="aa-events-agenda-title"><?php echo esc_html( $occurrence['title'] ); ?></span>
<time datetime="<?php echo esc_attr( aa_events_occurrence_start( $occurrence )->format( DATE_W3C ) ); ?>">
<?php echo esc_html( aa_events_format_occurrence( $occurrence ) ); ?>
</time>
</a>
</li>
Render a localized “No events this month.” message instead of an empty list.
- Step 4: Remove dead output-buffer code
Delete $originalEchoDayCell, $calendarRows, $calendarOutput, and the unused preg_replace() block. Confirm no output buffering remains in the template.
- Step 5: Run syntax and helper tests
Run:
php tests/run.php
php -l templates/calendar.php
Expected: tests pass and calendar template has no syntax errors.
- Step 6: Commit calendar markup when Git is available
git add templates/calendar.php
git commit -m "feat: render spanning calendar event bars"
Task 7: Style Spanning Bars and the Mobile Agenda
Files:
-
Modify:
assets/css/aa-events.css -
Modify:
templates/aa-events.css -
Step 1: Replace table-only calendar styling
Implement these layout contracts in assets/css/aa-events.css:
.aa-events-calendar-grid { border-left: 1px solid #aaa; border-top: 1px solid #aaa; }
.aa-events-calendar-week { position: relative; }
.aa-events-calendar-days,
.aa-events-calendar-weekdays { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
.aa-events-calendar-day {
border-bottom: 1px solid #aaa;
border-right: 1px solid #aaa;
min-height: calc(4.5rem + var(--aa-event-lanes, 1) * 2rem);
padding: .625rem;
}
.aa-events-calendar-bars {
display: grid;
gap: .25rem 0;
grid-template-columns: repeat(7, minmax(0, 1fr));
grid-template-rows: repeat(var(--aa-event-lanes), 1.75rem);
left: 0;
padding: 2.25rem .2rem .5rem;
pointer-events: none;
position: absolute;
right: 0;
top: 0;
}
.aa-events-calendar-event {
background: #0969da;
border-radius: .25rem;
color: #fff;
grid-column: calc(var(--aa-event-column) + 1) / span var(--aa-event-span);
grid-row: calc(var(--aa-event-lane) + 1);
overflow: hidden;
padding: .2rem .45rem;
pointer-events: auto;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
}
.aa-events-calendar-event.is-continuing-before { border-bottom-left-radius: 0; border-top-left-radius: 0; }
.aa-events-calendar-event.is-continuing-after { border-bottom-right-radius: 0; border-top-right-radius: 0; }
.aa-events-calendar-event:focus-visible { outline: 3px solid #111; outline-offset: 2px; z-index: 2; }
.aa-events-calendar-day.is-outside-month { background: #f6f7f7; color: #646970; }
.aa-events-calendar-day.today-cell { background: #ffe9b3; box-shadow: inset 0 0 0 2px #f7b500; }
.aa-events-calendar-agenda { display: none; }
Adjust the week minimum height from --aa-event-lanes so absolute bars cannot overlap the following week. Preserve existing header, Today button, archive, and card-grid styles.
- Step 2: Add the mobile breakpoint
At max-width: 640px, hide .aa-events-calendar-desktop, show .aa-events-calendar-agenda, and style agenda links as full-width blocks with a visible left border, readable title/date hierarchy, and :focus-visible. Do not rely on color alone to identify links.
- Step 3: Synchronize the theme-copy stylesheet
Run:
cp assets/css/aa-events.css templates/aa-events.css
cmp assets/css/aa-events.css templates/aa-events.css
Expected: cmp exits with status 0.
- Step 4: Verify CSS structure
Run:
rg -n "aa-events-calendar-(grid|week|event|agenda)" assets/css/aa-events.css
cmp assets/css/aa-events.css templates/aa-events.css
Expected: all four component selectors are present and both stylesheets are identical.
- Step 5: Commit responsive styling when Git is available
git add assets/css/aa-events.css templates/aa-events.css
git commit -m "style: add responsive multi-day calendar layout"
Task 8: Document the New Editing and Theme Behavior
Files:
-
Modify:
README.md -
Step 1: Rewrite the multiple-date documentation
Rename “Multiple Event Dates” to “Event Occurrences.” Document that each row supports a required start date, optional times/end date, and all-day mode. Include one example combining a Friday–Sunday occurrence with a separate later occurrence.
- Step 2: Document responsive calendar behavior
State explicitly that desktop/tablet retain the traditional month grid with week-spanning bars and screens at or below 640px use an agenda list.
- Step 3: Document custom ACF groups and theme overrides
List required repeater/subfield names: event_dates, date, start_time, end_date, end_time, all_day. Explain that an existing theme override of aa-events/calendar.php remains authoritative and must be updated to opt into spanning bars, while it can use aa_events_get_occurrences() immediately.
- Step 4: Check documentation terminology
Run:
rg -n "multiple_dates|Event Dates repeater|multi-day|Occurrences|640px" README.md
Expected: no instructions tell editors to use multiple_dates; occurrence and responsive behavior are documented.
- Step 5: Commit documentation when Git is available
git add README.md
git commit -m "docs: explain event occurrence ranges"
Task 9: Final Verification
Files:
-
Verify all modified PHP, JavaScript, CSS, tests, templates, and documentation.
-
Step 1: Run the complete automated suite
Run:
php tests/run.php
find . -path './.superpowers' -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l
cmp assets/css/aa-events.css templates/aa-events.css
Expected: all AA Events tests pass, every PHP file reports no syntax errors, and CSS files compare equal.
- Step 2: Verify field-name consistency
Run:
rg -n "multiple_dates" --glob '!docs/IMPLEMENTATION_PLAN_REPEATER.md' .
rg -n "event_dates|end_date|end_time|all_day" includes templates README.md
Expected: no production references to multiple_dates; all new fields are present in registration, normalization, templates, and documentation.
- Step 3: Perform WordPress editor checks
In the local WordPress site:
- Create one event with a timed Friday 6 PM through Sunday 2 PM occurrence.
- Add a second all-day occurrence to the same event.
- Confirm all-day disables/clears time inputs.
- Attempt an end before the start and confirm a row-numbered save error.
- Save a legacy-only event and confirm exactly one occurrence row is created.
- Save it again and confirm no duplicate row appears.
Expected: editing, validation, and migration match the design.
- Step 4: Perform calendar and accessibility checks
- View a month where an occurrence crosses a week and confirm the title repeats on both segments.
- View occurrences crossing previous/next month boundaries and confirm continuation indicators.
- Create overlapping ranges and confirm separate lanes with no covering.
- Change WordPress “Week Starts On” between Sunday and Monday and confirm segment columns remain correct.
- Keyboard-tab through event bars and confirm visible focus and complete accessible labels.
- At 641px confirm the traditional grid; at 640px confirm the agenda and one item per occurrence.
- Confirm Today and previous/next navigation update both presentations.
Expected: all visual, responsive, and keyboard behaviors match the approved specification.
- Step 5: Commit final corrections when Git is available
git add -A
git commit -m "fix: complete multi-day occurrence verification"
Only create this commit if verification required corrections; otherwise leave the previous focused commits as the complete history.