# BW Pricing Card — Architecture

## Overview

Single-block plugin. One PHP entry point registers the block from its `block.json` manifest; everything else is self-contained inside `blocks/bw-pricing-card/`. The block is server-rendered, so stored post content is just `<!-- wp:bw/pricing-card {...attributes} /-->` — the final HTML is regenerated on every page load from `render.php`. CSS or markup tweaks in a future plugin version apply retroactively to every existing use of the block, with no content migration needed.

## File Layout

```
wp-content/plugins/bw-pricing-card/
├── bw-pricing-card.php          Main file — plugin header, constants, hooks
├── uninstall.php                No-op (no persistent state to clean up)
├── README.md                    User-facing
├── CLAUDE.md                    Dev guide
├── CHANGELOG.md                 Keep-a-Changelog
├── LICENSE                      GPL-2.0-or-later
├── blocks/
│   └── bw-pricing-card/
│       ├── block.json           Manifest — cards repeater attribute, asset refs
│       ├── index.js             Editor UI — window.wp.* globals, no JSX
│       ├── index.asset.php      Declares editor script dependencies
│       ├── render.php           Server-side render callback
│       ├── style.css            Frontend + editor shared styles
│       └── editor.css           Editor-only chrome
└── docs/                        Dev docs, not shipped to end users
```

## Core Components

| Component | Responsibility | File |
|---|---|---|
| `bw_pricing_card_register_block()` | Registers the block from block.json on `init` | `bw-pricing-card.php` |
| `bw_pricing_card_load_textdomain()` | Loads translations on `plugins_loaded` | `bw-pricing-card.php` |
| Editor UI | Inline RichText per text field; InspectorControls for structural fields; per-card panels with move/remove; canvas `+ Add card` tile | `blocks/bw-pricing-card/index.js` |
| Render template | Validates `buttonStyle` against enum, filters RichText fields with `wp_kses`, emits the grid markup | `blocks/bw-pricing-card/render.php` |

## Attributes

Declared in `block.json` under `"attributes"`:

| Name | Type | Default | Notes |
|---|---|---|---|
| `cards` | array | 3 default tiers (Early Bird / General Admission / Team of 5+) | Each entry: `{ tier, price, description, items[], buttonText, buttonUrl, buttonStyle, isPopular }` |
| `popularLabel` | string | `"Most Popular"` | Text on the gradient ribbon for popular cards |

Per-card `buttonStyle` is enum-constrained server-side to: `fill-base`, `theme-base`, `secondary-base`, `outline-base`.

## Hooks

### Actions
- `init` — `bw_pricing_card_register_block` registers the block.
- `plugins_loaded` — `bw_pricing_card_load_textdomain` loads translations.

### Filters
None.

## Data Storage

No options, transients, custom tables, or post meta. Attribute values are serialized inside post content as standard block-comment delimiters.

## External Dependencies

None. Uses only built-in WordPress script handles (`wp-blocks`, `wp-element`, `wp-block-editor`, `wp-components`, `wp-i18n`) declared in `blocks/bw-pricing-card/index.asset.php`.

## Security Notes

- **`buttonStyle`** is whitelisted server-side in `render.php` against the four-value enum before being concatenated into a class name; never trusted directly.
- **RichText fields** (tier, price, description, items, button text) are filtered through `wp_kses()` rather than `esc_html()`. Reason: RichText stores HTML-encoded entities (e.g. `Hello &amp; World`); `esc_html()` would double-encode them. The allowlist is empty (`array()`) for short labels — strips all tags but preserves entities — and limited to `<em><strong><b><i>` for description and feature items.
- **`buttonUrl`** is escaped through `esc_url()`.
- **Class names** flow through `esc_attr()`.
- Wrapper attributes come from WordPress's own `get_block_wrapper_attributes()` which handles escaping.
- The editor renders the button as `<a>` without `href` (only `tagName`), so RichText click-to-edit isn't intercepted by link navigation.

## CSS Specificity Notes

Two rules in `style.css` use chained-class selectors plus `!important` to beat parent-theme defaults:

- `.bw-pricing-card__list` — kills `padding-left` and `padding-inline-start` (Kadence sets `padding-inline-start` on `.entry-content ul`).
- `.bw-pricing-card__desc` — pins `margin-bottom: 16px` against the theme's default `p` margin.

These are documented inline. Any new property that the theme might override should follow the same pattern.
