# BW Agenda Table — Architecture

## Overview

Single Gutenberg block (`bw/agenda-table`) registered via `block.json` with a server-side `render.php`. Editor preview uses `ServerSideRender` so editors see exact frontend output without divergent React markup.

## File Layout

(See §3 of PLUGIN.md for the canonical layout.)

```
wp-content/plugins/bw-agenda-table/
├── bw-agenda-table.php             Header, constants, bootstrap
├── uninstall.php                   Option scrub on plugin delete
├── README.md                       User-facing
├── CLAUDE.md                       Claude dev guide
├── CHANGELOG.md                    Keep-a-Changelog
├── LICENSE                         GPL-2.0-or-later notice
├── includes/
│   └── class-bw-agenda-table-block.php   Block registration + asset enqueue
├── blocks/bw-agenda-table/
│   ├── block.json                  Block manifest
│   ├── render.php                  Server-side render (helpers + markup)
│   ├── index.js                    Editor entry (Inspector + ServerSideRender)
│   ├── view.js                     Frontend interactivity (multi-day switcher)
│   ├── style.css                   Frontend + editor shared styles
│   ├── editor.css                  Editor-only tweaks
│   └── index.asset.php             Editor script dependency manifest
└── docs/
    ├── SPEC.md
    ├── ARCHITECTURE.md             (this file)
    ├── TESTING.md
    ├── ROADMAP.md
    ├── KNOWN-ISSUES.md
    ├── HANDOFF-NOTES.md
    └── SESSION-LOG.md
```

## Core Classes

| Class | Responsibility | File |
|---|---|---|
| `BW_Agenda_Table_Block` | Hooks `register_block_type`, conditionally enqueues Material Symbols on the frontend (only when block is present) and unconditionally in the editor | `includes/class-bw-agenda-table-block.php` |

## Render Pipeline (render.php)

1. **Resolve & sanitize attributes** — clamp enums to allowed values, cast numerics.
2. **Define helpers** (file-scoped, `function_exists` guarded so re-render in the same request is safe):
   - `bw_agenda_table_get_first_field()` — alias-resolving ACF / post-meta read.
   - `bw_agenda_table_normalize_date()` / `_normalize_time()` — ACF Date/Time Picker raw → canonical ISO.
   - `bw_agenda_table_format_time()` / `_format_meta_line()` / `_format_day_heading()` / `_format_date_range()` — display formatting.
   - `bw_agenda_table_track_color_class()` — term meta `color_class` → CSS class.
   - `bw_agenda_table_query_sessions()` — `WP_Query` for `session` CPT, returns `null` when CPT absent.
   - `bw_agenda_table_demo_rows()` — fallback dataset for empty/missing CPT.
   - `bw_agenda_table_row_sort_key()` / `_sort_rows()` — chronological sort by date+time.
   - `bw_agenda_table_group_by_day()` / `_group_by_slot()` — bucket rows for rendering.
3. **Build dataset** — query → fall back to demo if empty/null → apply demo track filter → sort → limit → group by day.
4. **Resolve date-nav mode + header label** — Auto resolves to single/multi from day count.
5. **Render** — header (nav + label), per-day section (heading + timeline), per-slot wrapper, per-row card.

## Frontend JS (view.js)

Boots once on `DOMContentLoaded`. For each `.bw-agenda-table` element:

1. Skip if fewer than 2 day-sections (single-day, no switching needed).
2. Hide all but the first day section.
3. Wire trigger button → toggle listbox.
4. Wire listbox option click/keyboard → setActive(key).
5. Wire prev/next arrows → navigate by index in `keys[]`.
6. Outside-click + Escape close the listbox.

Progressive enhancement: with JS off, no `[hidden]` attributes are set, all days render stacked.

## Hooks

### Actions

This plugin only consumes WordPress core actions (`init`, `wp_enqueue_scripts`, `enqueue_block_editor_assets`, `plugins_loaded`). It does not currently fire any custom `do_action`.

### Filters

None yet. Likely future filter points:
- `bw_agenda_table_query_args` — let host sites tweak the `WP_Query` args.
- `bw_agenda_table_session_row` — let host sites enrich a normalised row before render.
- `bw_agenda_table_field_aliases` — let host sites add field-name aliases.

## Data Storage

This plugin **does not** create options, post meta, or custom tables of its own. All Session data is owned by the host site (CPT + ACF/post meta + `track` taxonomy). The `uninstall.php` scrub is purely defensive, in case future versions add an options array.

## External Dependencies

- `Material Symbols Outlined` Google Font (loaded conditionally on the frontend, unconditionally in the editor).
- WordPress core JS packages (`wp-blocks`, `wp-element`, `wp-block-editor`, `wp-components`, `wp-i18n`, `wp-server-side-render`).
- Optional: ACF (Advanced Custom Fields) — falls back to `get_post_meta()` when absent.

## Security Notes

- All output values are passed through `esc_html` / `esc_attr` / `esc_url` / `sanitize_html_class` before output.
- Track filter is sanitised through `sanitize_title()`.
- Limit is clamped via `max( 0, (int) ... )`.
- `dateNavigationMode` and `clickAction` are clamped to allow-lists.
- The block has no admin POST handler, no AJAX endpoint, no REST route — it only reads on render.
- The single `phpcs:ignore` for `WordPress.Security.EscapeOutput.OutputNotEscaped` is on `get_block_wrapper_attributes()`, which returns pre-escaped output by core contract.
