# BW Dev — Specification

**Version:** 0.1.0 | **Last Updated:** 2026-05-11

## Purpose

BW Dev is the single WordPress plugin Bowden Works uses on every Kadence-based site. It bundles the small admin and editor utilities that previously lived in five separate plugins (`bw-admin-column`, `bw-favicon`, `bw-pretty-post-link`, `bw-sticky-settings`, `bw-youtube-embed`) into one BW-compliant plugin with a unified settings page and white-label support.

See `docs/notes-from-adi.txt` for the original brief.

## Scope — what's IN 0.1.0 → 1.0.0

Five modules, each ported from an existing plugin and re-prefixed:

| Module slug | UI label | Source plugin | Type |
|---|---|---|---|
| `admin_columns` | Admin Columns | bw-admin-column | Admin |
| `favicon` | Favicon | bw-favicon | Admin + frontend |
| `sticky` | Sticky Elements | bw-sticky-settings | Admin + frontend |
| `post_link` | Post Link Block | bw-pretty-post-link | Editor (Gutenberg) |
| `youtube` | YouTube Block | bw-youtube-embed | Editor (Gutenberg) + shortcode |

White-label layer (Phase 7) — see "White-label" below.

## Out of scope

- The 5 source plugins remain on disk side-by-side during development. Auto-deactivation, deletion, or any modification of them is OUT of scope for this plugin.
- Migration of legacy options (Phase 8) is one-way: read old option → write new option → leave old option in place. The user deactivates old plugins manually once verified.
- New features beyond what the 5 source plugins already do are NOT in scope for 1.0.0. Add ideas to `docs/ROADMAP.md` "Future".

## Requirements

### Functional

**R1 — Tabbed settings page at Settings → BW Dev.** A single submenu under the WordPress Settings menu, with tabs for: Modules, Branding, and one tab per enabled module. Capability: `manage_options`.

**R2 — Per-module enable/disable.** The Modules tab has a checkbox per module. Disabled modules register NO hooks, NO assets, NO REST routes. Default: all enabled.

**R3 — Single root option `bw_dev_settings`.** All persisted state lives under this option as a nested array. Per-module data is keyed by module slug, e.g. `['favicon' => ['url' => '...']]`. Per-module sanitize callbacks run on save.

**R4 — Admin Columns module.** Configure custom admin list columns per post type (taxonomies, meta fields, featured image with inline edit). Feature parity with `bw-admin-column`.

**R5 — Favicon module.** Inject a single custom favicon PNG at priority 9999 on `wp_head`, `admin_head`, `login_head`. Feature parity with `bw-favicon`. Must add the `sanitize_callback` the original was missing.

**R6 — Sticky Elements module.** Multi-element sticky configuration by CSS selector (offset, z-index, margin-bottom, push-up element, mobile disable, breakpoint). Feature parity with `bw-sticky-settings`.

**R7 — Post Link Block module.** Two Gutenberg blocks (`bw-dev/post-link-list` parent, `bw-dev/post-link-item` child) with Simple and Thumbnail layouts. Feature parity with `bw-pretty-post-link`. REST endpoint renamed `bw-dev/v1/post-types`.

**R8 — YouTube Block module.** Gutenberg block `bw-dev/youtube` (server-rendered, dynamic) + shortcode `[bw_dev_youtube]` (with backwards-compat alias `[bw_youtube]`). Reads URL from a configurable ACF field. ACF field name MUST be configurable per-block instance, with a global default in settings.

**R9 — Brand resolver.** A single PHP filter `bw_dev_brand` returns the resolved branding config:
- `plugin_display_name` (default `BW Dev`) — shown in plugin list + Settings submenu.
- `block_category_label` (default `BW Blocks`) — Gutenberg category title for all bw-dev blocks.
- `block_title_prefix` (default `BW `) — prepended to each block's display title (set to blank to omit).

White-label scope is intentionally limited to blocks + plugin admin name (not internal settings tab labels) — clients don't visit `Settings → BW Dev`.

**R10 — Block JS filter for white-label.** A JS-side filter on `blocks.registerBlockType` rewrites `title` and `category` for every block whose name starts with `bw-dev/`. The configured brand is exposed via `wp_add_inline_script` before the editor bundle loads.

### Non-functional

- WordPress 6.0+ / PHP 7.4+.
- Disabled modules add zero runtime cost (no hook registration, no asset enqueue, no REST route).
- All output escaped per BW SECURITY.md. All AJAX/admin-post handlers have nonce + capability check.
- `cleanup-scan.sh`, `security-scan.sh`, `test-plugin.sh` all pass before 1.0.0.
- Translatable: every user-visible string uses `__()` / `_e()` with text domain `bw-dev`.
- No external HTTP calls from PHP at runtime. The bundled `plugin-update-checker` is the only outbound network call and only when WordPress runs its update check.

## Acceptance Criteria

| R | Acceptance | Status |
|---|---|---|
| R1 | `Settings → BW Dev` submenu renders tabs; current tab highlighted; only `manage_options` users can see it. | ☐ |
| R2 | Toggling a module off removes its hooks immediately on next page load (verified by `xdebug_get_function_stack`-style inspection or by feature disappearing). | ☐ |
| R3 | `get_option('bw_dev_settings')` returns nested array; old `bw_favicon_url` etc. are unread once migrated. | ☐ |
| R4 | Side-by-side test on dev site: bw-dev Admin Columns produces same column output as old bw-admin-column for a CPT with taxonomy + meta + image. | ☐ |
| R5 | Favicon URL persists, renders at priority 9999, survives multisite-like edge cases. Sanitize callback rejects non-URL input. | ☐ |
| R6 | Sticky Element with offset/z-index/margin/push/mobile-disable all behave identically to bw-sticky-settings. | ☐ |
| R7 | Both blocks render in editor + frontend with identical output to bw-pretty-post-link. REST endpoint returns same post-type list. | ☐ |
| R8 | YouTube block in editor + on frontend renders identical iframe to bw-youtube-embed. ACF field name is settable per-block. `[bw_youtube]` alias still works. | ☐ |
| R9 | Changing `block_title_prefix` in Branding tab changes the editor's inserter labels without rebuild. | ☐ |
| R10 | `bw_dev_brand` filter applied in PHP changes the Settings submenu label and plugin list display name. | ☐ |

## Migration (Phase 8)

On `register_activation_hook`:
1. If `bw_dev_settings` does not exist, build it by reading: `bw_favicon_url`, `bw_admin_column_settings`, `bw_sticky_settings`, `bw_youtube_embed_settings`.
2. Write the new nested option.
3. Do NOT delete the old options. The user manually deactivates the old plugins once verified; their uninstall hooks will clean up legacy options.
