# UI Architecture Rules — International Press Zone

Audience: AI coding agents first.

Canonical architecture decision document. One source of truth: inventory, audit, showcase, and enforcement point here. NEVER duplicate doctrine in plans, specs, or skills.

Scope: `admin/src/**` SPA modules, `includes/Admin/*.php` admin surfaces, and every PHP file that renders admin-visible UI outside that dir: editor `includes/Translation/MetaBox.php`, admin notices (`includes/Translation/BulkActions.php`, `includes/Licensing/UpdateChecker.php`, `includes/Compatibility/CompatibilityManager.php`), admin list column `includes/Compatibility/WooCommerceIntegration.php`, and privacy policy content `includes/Audit/ComplianceManager.php`. Skip: node_modules, demos, docs, tests (test rules in R-UI-8). New PHP file that renders admin-visible UI MUST be added to the shared surface list in `tools/ui-php-surfaces.mjs` (consumed identically by `tools/ui-inventory.mjs` and `tools/ui-gate.mjs`) and classified in `ui-layers.json`.

## R-UI-1 — Three layers

Classify every UI module as exactly one layer:

1. Primitive: reusable control or visual unit; zero feature/domain knowledge. Examples: Button, Input, SearchControl, Badge, Modal.
2. Composition: arranges primitives into reusable regions; no feature policy or API ownership. Examples: PageShell, PageHeader, FilterBar, DataRegion, table region.
3. Feature: owns one bounded domain workflow; may compose primitives/compositions and call feature services. Examples: ACF field policy workspace, bulk translation workflow.

Also: `special` = editor/server-rendered surface with distinct mount/security contract; `infra` = non-UI utility (api, dom, logger, stores, barrel index).

Import flow MUST be feature → composition → primitive. Primitive MUST NOT import composition or feature. Composition MUST NOT import feature. Exceptions: R-UI-9 only. Special/infra exempt from direction checks but MUST be classified.

Classification lives in `docs/architecture/ui-layers.json`. New module without entry = violation.

## R-UI-2 — Single state owner

Every state value MUST have exactly one owner. Per state value, a component MUST choose exactly one mode:

- controlled: receive value, emit action; NEVER mutate source directly;
- local: own value and lifecycle; NEVER accept competing external source.

DO NOT mirror the same state across page and component. Explicit synchronization adapter = R-UI-9 exception.

## R-UI-3 — Unidirectional data flow

Pass state down. Emit actions up. Components MUST NOT mutate unrelated page state, reach into sibling internals, or trigger whole-page repaint. Page/feature owner applies action, updates state, then updates declared region.

DO NOT:
```js
SearchControl({
    onInput: query => { state.query = query; paint(); },  // whole-page repaint
});
```
DO:
```js
search.onQueryChange(query => {
    state.query = query;
    resultsRegion.update(renderFilteredResults());        // declared region only
});
```

## R-UI-4 — Stateful lifecycle

Every stateful component MUST expose a consistent `mount()`, `update(next)`, `destroy()` interface. Factory-created already-mounted components MUST return equivalent handle. `destroy()` MUST be idempotent and release owned listeners, observers, timers, animation frames, requests, temporary DOM, body/document mutations, and focus ownership. Stateless primitives MAY return DOM only and MUST be classified stateless.

## R-UI-5 — Configuration complexity budget

Reject universal component when ANY holds:

- more than 3 independent mode/variant flags;
- boolean flags create more than 4 meaningful combinations;
- an option changes DOM hierarchy, state ownership, and business behavior together;
- consumer requires callbacks/options used by only one caller;
- component has feature-name branching.

Split into smaller composed components. Bounded documented visual enums do not count as mode flags. Budget exception: R-UI-9 record with focused tests.

## R-UI-6 — Styling boundary

Component SCSS owner controls component root and descendants. Pages control placement around component via documented wrapper/layout API only. Page MUST NOT target component internals, duplicate component selectors, or use specificity overrides. Component MUST NOT set page placement. Dynamic geometry/state MUST use documented attributes/classes/CSS variables. Inline presentation forbidden.

## R-UI-7 — Design tokens

All reusable spacing, typography, colors, borders, radii, elevation, breakpoints, motion MUST resolve to canonical tokens. New raw values require token decision, not local literal. Token aliases MAY bridge migration only with R-UI-9 owner and removal condition.

## R-UI-8 — Testing responsibility

Test behavior once, at lowest owning layer:

| Layer | Required tests | MUST NOT repeat |
|---|---|---|
| Primitive | interaction, variants, keyboard, ARIA, focus, lifecycle | feature business predicates |
| Composition | regions, state boundaries, update scope, mount/update/destroy integration | primitive interaction matrix |
| Feature | domain state, API actions, routing, filtering, errors, workflow | exhaustive primitive variants |
| Browser | representative cross-layer journeys, responsive layout, theme, real focus | full unit permutation matrix |

Higher layer MAY assert critical integration seam. MUST NOT duplicate lower-layer suite.

## R-UI-9 — Exception budget

Every exception MUST record: rule ID, owner, reason, exact callers, creation date, and exactly one of expiry (future ISO date) or removalCondition (measurable removal condition). New work gets zero unexplained exceptions. Enforcement MUST fail when exception expires, caller disappears, scope widens, or required metadata missing.

Registry: `docs/architecture/ui-exceptions.json`, validated by `tools/ui-gate.mjs`. Empty = healthy.

## R-UI-10 — Component showcase

Maintain development-only showcase covering every primitive and composition: all supported variants, loading/empty/error/disabled states, light/dark themes, responsive boundaries, reduced motion, keyboard/focus, accessibility semantics. Showcase MUST use production exports and styles; NEVER fork demo-only markup. Exclude showcase from production runtime/bundle.

---

## Enforcement & evidence

| Artifact | Path | Contract |
|---|---|---|
| Rules (this doc) | `docs/architecture/ui-architecture-rules.md` | canonical; amend only by decision |
| Layer registry | `docs/architecture/ui-layers.json` | every module, exactly one layer |
| Exception registry | `docs/architecture/ui-exceptions.json` | R-UI-9 schema |
| Generated inventory | `docs/architecture/ui-inventory.report.json` + `.md` | `node tools/ui-inventory.mjs`; deterministic from clean checkout; `--verify` fails on drift |
| Changed-file gate | `tools/ui-gate.mjs` | blocks new violations on changed files; measured legacy baseline in base content allowed; wired into `tools/admin-gate.sh` |
| Component audit | `docs/ui-component-audit.md` | manual prose; counts point to generated inventory, never hand-numbers |

Rule of thumb: if a claim is a number or a list of modules, generate it. If it is policy, put it here.
