# UI Component Audit — International Press Zone

Audit date: 2026-08-14
Scope: `admin/src/` SPA (main UI surface) + shared component library at `admin/src/components/`.
PHP templates (`includes/Admin/*.php`) only emit mount points + asset handles — no markup violations (verified).

---

## Design system status (the root problem)

Two class systems coexist. The **new** system is `presszone-international-*` (the Press.Zone design-system family); `ipz-*` / `international-*` are **legacy** homegrown classes.

Census of `admin/src/components/*.js` (text-domain strings excluded):

| Status | Count | Components |
|---|---|---|
| New system only | 11 | Button, Card, Checkbox, EmptyState, FormField, Input, Select, Spinner, Textarea, Toggle, Tooltip |
| Mostly new (stragglers) | 4 | Tabs, StatCard, Table, RegionPresets |
| Mixed — shell still legacy | 2 | Modal (1 new / 21 legacy — emits `ipz-modal*`), PageHeader (1 / 5 — still `ipz-page-header`) |
| Legacy only | 27 | AnimatedItem, AssignmentCard, Badge, BulkActionBar, ColorField, ColorPickerModal, DataRegion, DeadlineIndicator, ErrorState, ExceptionsTab, FilterBar, FlagPicker, GridTable, LinkSelector, Notice, Placeholders, PriorityBadge, ProgressBar, Skeleton, SortableList, StateTransitionButton, StatusFeedback, TagInput, TileSelect, Toast, TranslationProgress, WorkflowTimeline (18 of these are deleted in §3) |
| Classless | 1 | PageShell |

75% of the library still emits legacy classes — including the Modal shell every page imports. Pages that correctly use shared components (Badge, Modal, Toast, ProgressBar, Skeleton, DataRegion, ...) therefore render legacy-styled UI. **The new design system cannot become dominant until the surviving components are migrated.**

### How "new" vs "legacy" was determined

- **Declared in code**: `styles/main.scss:8-10` — "The SPA uses the Press.Zone design system classnames (`presszone-international-*`). Some legacy pages still use `ipz-*` / `international-*`; we keep their styles for now."
- **SCSS provenance**: `styles/_components.scss:3-4` — "Comments Press Zone - Components, Converted from main.css lines 560-900". The `presszone-*` prefix is the monorepo design-system family (comments-press-zone uses `presszone-comments-*`); `ipz-*` / `international-*` are the plugin's homegrown classes.
- **Not evidence**: git recency — everything arrived in the initial import; no migration commits exist.

---

## 1. Violations scan — raw markup bypassing shared components

| # | Priority | File:line | Raw markup (legacy class) | Shared component that should be used | Recommended fix |
|---|---|---|---|---|---|
| V1 | High | `pages/dashboard.js:243` | hand-rolled status badge span (`presszone-international-badge`) | `Badge` | Replace span with `Badge`; variant mapped from status (success/warning/danger) |
| V2 | High | `pages/my-assignments.js:62-65` | hand-rolled page header (`ipz-page-header__*`) | `PageHeader` (used by 8 other pages) | Replace with `PageHeader`, title + description, matching the other pages' usage |
| V3 | High | `pages/my-assignments.js:354-373`, `pages/licensing.js:494-504, 627-639` | hand-rolled form group / input / textarea / help (`ipz-form-group`, `ipz-input`, `ipz-textarea`, `ipz-form-help`) | `FormField` (from Input.js), `Input`, `Textarea` | Wrap each control in `FormField(key, label, help)`; use `Input` and `Textarea` (my-assignments.js:373 currently hand-rolls `ipz-textarea` though Textarea.js exists) |
| V4 | High | `pages/translate-history.js:223-259` | hand-rolled filter bar + raw input mixing `ipz-filter-input` with `presszone-international-input--settings` | `FilterBar`, `Input` (class mix in one node proves both systems raced) | Replace filter bar with `FilterBar` and its `Input`; remove `ipz-filter-input` |
| V5 | Medium | `pages/licensing.js:258-279` | hand-rolled status card (`ipz-license-status__*`) | `Card` / `StatCard` | Replace status card with `Card` (title/status/rows) + `Badge` for the status pill |
| V6 | Medium | `pages/onboarding.js:262-298, 778+` | hand-rolled tier cards, billing toggle, checkout summary (`ipz-tier-card`, `ipz-billing-toggle`, `ipz-checkout-summary`) | `Card`, `Toggle`, partial extraction | Tier card → `Card` layout; billing toggle → `Toggle`; checkout summary → `Card` (could extract an `OnboardingTierCard` component) |
| V7 | Medium | `utils/promo-modal.js:23` | hand-rolled modal content (`ipz-promo-modal__*`, legacy classes) | `Modal` shell (already used) + migrated classes | Keep Modal shell, swap `ipz-promo-modal__*` classes for the new modal classes |
| V8 | Medium | `editor/translation-metabox.js:176-201` (+24 more) | standalone hand-rolled editor UI (`ipz-editor-translation__*`); imports no shared component, own SCSS | shared components or at least new classes | Migrate to shared `TranslationProgress`/`Toast`/`Modal` where possible; at minimum swap classes to new system |
| V9 | Medium | `sections/analytics-dashboard.js:342` | `presszone-international-table--legacy` class flag | remove marker once Table is migrated | Delete `presszone-international-table--legacy` and its SCSS when legacy routing is gone |
| V10 | Low | `pages/migration.js`, `pages/TeamDashboard.js`, `pages/WorkflowDashboard.js` | scattered `ipz-*` structural classes (steps, metrics, cards) | migrate classes; mostly unique structures | Swap remaining `ipz-*` structural classes to `presszone-international-*` equivalents |

Violation tallies: V1–V4 attack paths that would disappear completely if the shared components were used correctly — 4 pages bypass the library on 6+ nodes each. V5–V8 are contained to single pages/components. V9–V10 are class-level cleanup.

---

## 2. Cross-file duplication scan — patterns repeated across 3+ files

| # | Priority | Pattern | Files (representative lines) | Recommendation |
|---|---|---|---|---|
| D1 | Critical | Bulk "Translate All" modal flow: modal content + bulk progress container + per-language result rows (`ipz-generate-all-modal`, `ipz-bulk-progress-container`, `ipz-translate-all-result`) — ~150-300 lines each | `pages/content-translate.js:763-944`, `pages/string-translate.js:1319-1330, 1423-1435, 1497-1600`, `pages/translate-posts.js:1059+` | Extract `BulkTranslateModal` shared component (modal shell + progress + results), parameterized by content/string/post source |
| D2 | High | Side-by-side edit grid: original/translation panes + generating/failed states (`ipz-st-edit-grid__original/translation`, `ipz-ct-generating`) | `pages/content-translate.js:1329-1438`, `pages/string-translate.js:876-919`, `pages/translate-posts.js:716-820` | Extract `EditGrid` / `TranslationRow` component (original pane, target pane, generating/failed/empty states) |
| D3 | Medium | Per-language result row markup (inside D1) | same 3 files | folds into `BulkTranslateModal` |

Note: `ipz-ct-generating`'s indeterminate bar hand-rolls exactly what `ProgressBar` already provides — D1/D2 extraction must use `ProgressBar`, not copy it again.

---

## 3. Component-level resolutions (the consolidated verdicts)

### 3.1 Provenance (verified in git history)

The 24-component core was copied wholesale from comments-press-zone's `admin/src-vanilla/components/` (identical filenames verified) into the predecessor `multilingual-press-zone` in Feb 2026 (bcd774bc7 "Align Multilingual admin UI with Comments settings", 2026-02-14), renamed to international-press-zone 2026-03-23 (c87a0dede). All 21 dead files were touched by **exactly one commit ever** — their import. Orphan timeline: 18 dead on arrival in the international era (pre-rename page rewrites, 8c5075e8d "dashboard overhaul, dead code cleanup" 2026-03-16); Notice orphaned 2026-03-24; StatusFeedback orphaned 2026-08-01; StatCard's removal exists only as **uncommitted working-tree changes** to dashboard.js (128+/347- vs HEAD). They are ancient legacy, not new bloat.

### 3.2 Decision rule

**Merge** = the deleted file contains unique functionality the winner lacks → fold it in. **Delete clean** = the winner already covers 100% of the loser's behavior, or the feature exists nowhere in the app.

Each pair below was judged on four lenses: **nicer** (visual quality), **design-system fit** (new-system class usage), **functionality** (features/behavior), **more developed** (engineering completeness).

### 3.3 Verdicts — duplicate pairs (full rationale)

| Pair | Nicer | Design-system fit | Functionality | More developed | Verdict & action |
|---|---|---|---|---|---|
| **Input.js vs FormField.js** | tie (near-identical rendering) | Input: new classes (winner). FormField: new classes too | FormField adds tooltipManager: desktop hover + mobile tap-toggle help tooltips. Input renders help as a static `presszone-international-tooltip` span with native title attr — no mobile behavior | FormField: 2 exports (FormField + tooltipManager mechanism); Input: single lib-referenced export. Input.js is what `index.js` and 3+ pages import | **Keep Input.js, merge FormField's tooltip into it** (as managed help tooltip with hover + tap), export `FormField` from Input.js exactly as today so `index.js`/pages change nothing. Delete FormField.js |
| **Table.js vs GridTable.js** | GridTable: anonymous div-grid; Table: real table semantics, per-column geometry | Table: new classes. GridTable: legacy `international-grid-table` | Table: `createRegionView` + `_morphChildren` diffing, per-cell `data-tcol` parity, `_managedAttrs` WeakMap (tabs, a11y, live region); SkeletonTable mirrors it. GridTable: static div table, no diffing | Table: 294 lines with region-view wiring; GridTable: 151 lines, zero unique features | **Keep Table.js, delete GridTable.js** (div-grid is its only feature and is obsolete) |
| **Skeleton.js vs Placeholders.js** | Skeleton: shimmer bars; Placeholders: static chip row | both legacy classes; Skeleton is the load-state pattern the app already uses | Skeleton: 10 presets (text/card/stat/row/table/form/statRow/list/chart/page), aria-hidden, layout-shift-safe geometry, DataRegion integration. Placeholders: copyable `{{tag}}` chips — **feature exists nowhere in the app** (all "placeholder" hits are HTML input attributes) | Skeleton: 330 lines, actively extended (SkeletonPage, SkeletonChart). Placeholders: untouched | **Not a duplicate pair.** Keep **Skeleton.js** (14 importers incl. DataRegion); **delete Placeholders.js as dead code** — nothing to merge |
| **ProgressBar.js vs TranslationProgress.js** | ProgressBar: themed bar with rAF animation; TranslationProgress: 24-line innerHTML template w/ raw WP `status-badge` class | ProgressBar: new classes. TranslationProgress: legacy `ipz-progress-*` | ProgressBar: determinate/indeterminate, rAF, onComplete, label/percentage toggles, **11 importers**. TranslationProgress: static progress + text, 0 importers | ProgressBar: maintained. TranslationProgress: untouched legacy | **Keep ProgressBar.js, delete TranslationProgress.js** |
| **Toast.js vs Notice.js vs StatusFeedback.js** | Toast: managed toasts; Notice: static inline box; StatusFeedback: pulse glyph | Toast: new classes. Notice/StatusFeedback: legacy | Toast: `show()` lifecycle, auto-dismiss, dismissible, variants (success/error/warning/info), `clearAll`, `aria-live` container, **14 importers**; header comment "Replaces duplicate addNotice/showToast implementations". Notice: fixed severity, no lifecycle. StatusFeedback: pulse glyph only | Toast: maintained. Notice/StatusFeedback: untouched legacy | **Keep Toast.js, delete Notice.js and StatusFeedback.js** |
| **Badge.js vs PriorityBadge.js** | tie (PriorityBadge builds on Badge) | Badge: new; PriorityBadge: composes Badge → new | PriorityBadge: low/normal/high/urgent config map (variant/icon/tooltip). Badge: variants default/primary/success/warning/danger/info, sizes, icon, setVariant, getElement, factory helpers, **10 importers** | Badge: maintained. PriorityBadge: 0 importers, composes `new Badge()` so it is a config map, not a twin | **Keep Badge.js; add `priority` option** with the low/normal/high/urgent map, delete PriorityBadge.js |
| **EmptyState.js vs ErrorState.js** | EmptyState: themed empty state; ErrorState: legacy message? | EmptyState: new classes. ErrorState: legacy | EmptyState: icon/title/message (+action slot per site usage). ErrorState: error message + retry button | EmptyState: maintained. ErrorState: 0 importers | **Keep EmptyState.js; add error variant + retry button**, delete ErrorState.js |
| **Card.js vs StatCard.js** | StatCard: dashboard KPI widget, 4 color variants, trend/growth/comparison | StatCard: fully new-system (`presszone-international-widget`); Card: settings container, new | Card: settings pages container. StatCard: dashboard metrics incl. trend arrow, growth %, comparison | StatCard: dual API (default widget-classed + `renderStatCard` alternate); dashboard.js:113 `toStatCards` hand-rolls its own widget markup instead | **Keep both** (family, not rivals). Wire `toStatCards` onto StatCard's default API; delete `renderStatCard` + `_stat-card.scss` legacy classes (`.international-stat-card`, `.international-stats-row`) |

### 3.4 Verdicts — dead singles, zero importers (delete as dead code)

| Delete | Evidence |
|---|---|
| AnimatedItem.js | stagger/fade-in already built into Card.js:28 (`presszone-international-stagger-N`) |
| FlagPicker.js | languages.js:289 `renderFlag()` does its own emoji flags |
| LinkSelector.js + LinkSelector.example.js + demo/READMEs | imported only by its own demo |
| SortableList.js + example + test + docs | imported only by its own demo/test (SortableList.test.js is green but exercises nothing in the app) |
| TagInput.js, TileSelect.js | never imported |
| ColorField.js + ColorPickerModal.js | `openColorPicker` used only by ColorField itself |
| DeadlineIndicator.js, StateTransitionButton.js | never imported (both legacy) |

### 3.5 Keep list — used, survive (27 files)

AssignmentCard, Badge, BulkActionBar, Button, Card, Checkbox, DataRegion, EmptyState, ExceptionsTab, FilterBar, Input, Modal, PageHeader, PageShell, ProgressBar, RegionPresets, Select, Skeleton, Spinner, StatCard, Table, Tabs, Textarea, Toast, Toggle, Tooltip, WorkflowTimeline.

Coverage note: Textarea.js is imported by nothing today (my-assignments.js:373 hand-rolls `ipz-textarea`) — keep it (only labeled textarea, new-system styled) and fix the violation in V3 instead of deleting it.

Net effect: **45 component files → 27**, 18 deleted (3 merged, 15 dead), 1 pair wired.

---

## 4. Target state

- **27 components, one design system**: every surviving component emits `presszone-international-*` only.
- **Zero legacy classes** in `admin/src` outside intentional legacy routes.
- **No hand-rolled markup** where a component exists (V1-V8 closed).
- **BulkTranslateModal + EditGrid** extracted from the three translate pages (D1/D2 closed).
- `COMPONENT-LIBRARY.md` rewritten to the actual library (current doc is stale: documents the dead `multilingual-*` prefix, lists removed components, misses the `presszone-international-*` naming).

---

## 5. Fix plan (execution order)

1. **Merges** (§3.3): tooltip → Input.js; error/retry + error variant → EmptyState.js; priority config → Badge.js. Delete the three source files.
2. **Deletes** (§3.4): 15 dead components + demo/test/docs, their now-unreferenced `index.js` exports and SCSS (`admin/src/styles/components/*.scss` + `main.scss` legacy `@use` entries).
3. **Migrate legacy classes on the 27 survivors** → `presszone-international-*` + SCSS: Badge, Modal (21 legacy classes), PageHeader (5), Toast, ProgressBar, Skeleton, DataRegion, FilterBar, BulkActionBar, WorkflowTimeline, AssignmentCard, ExceptionsTab, Table/Tabs/StatCard/RegionPresets stragglers. Drop legacy refs from `styles/components/*.scss` in `main.scss`. Do not migrate deleted components (FlagPicker, LinkSelector, SortableList, etc.).
4. **Fix violations V1-V4** (Badge, PageHeader, FormField/Input/Textarea/FilterBar) + **V5-V6** (licensing status card → Card/Badge; onboarding → Card/Toggle).
5. **Extract D1 `BulkTranslateModal` + D2 `EditGrid`**; refactor the three translate pages onto them, using `ProgressBar`.
6. **Fix V7-V8** (promo-modal classes; translation-metabox → shared components or new classes).
7. Delete `presszone-international-table--legacy` (V9) once legacy routes are gone; migrate V10 scatter.
8. Rewrite `components/COMPONENT-LIBRARY.md` to the surviving 27-component library.

---

## 6. Verification

- Census must trend to zero: `rg -o "(?<!presszone-)international-[a-z-]+|ipz-[a-z-]+" admin/src -P | wc -l` (minus `international-press-zone` text-domain hits).
- Usage graph must be consistent: every `components/*.js` file either imported or listed as intentionally deleted. Re-run the import-resolution script after step 2.
- `cd admin && npm run build` after each step (no warnings).
- E2E: `tests/e2e/ui-matrix/`, `translations-page-load.spec.js`, `bulk-translate.spec.js`, `generate-translation.spec.js`, `translate-history.spec.js` cover the refactored surfaces.