# IPZ Admin Inline-UI Audit

Read-only audit completed. No repository files were modified.

Paths are relative to the repository root. Counts below are grouped finding types, not individual DOM nodes.

## 1. Summary

| File | Inline instances | Severity |
|---|---:|---|
| `admin/src/editor/editor-state.mjs` | 0 | Clean |
| `admin/src/editor/translation-state.mjs` | 0 | Clean |
| `admin/src/editor/translation-metabox.js` | 2 | Medium |
| `admin/src/main.js` | 0 | Clean |
| `admin/src/pages/TeamDashboard.js` | 1 | Low |
| `admin/src/pages/WorkflowDashboard.js` | 1 | Low |
| `admin/src/pages/account.js` | 0 | Clean |
| `admin/src/pages/analytics.js` | 0 | Clean |
| `admin/src/pages/content-translate.js` | 11 | High |
| `admin/src/pages/dashboard.js` | 3 | Medium |
| `admin/src/pages/fields-translate.js` | 2 | Low |
| `admin/src/pages/languages.js` | 1 | Low |
| `admin/src/pages/migration.js` | 6 | Medium |
| `admin/src/pages/my-assignments.js` | 5 | High |
| `admin/src/pages/onboarding.js` | 0 | Clean |
| `admin/src/pages/settings.js` | 2 | Medium |
| `admin/src/pages/site-content.js` | 10 | High |
| `admin/src/pages/string-translate.js` | 4 | Medium |
| `admin/src/pages/translate-history.js` | 5 | Medium |
| `admin/src/pages/translate-posts.js` | 8 | High |
| `admin/src/pages/translations.js` | 1 | Medium |
| `admin/src/sections/analytics-dashboard.js` | 5 | Medium |

## Component API baseline

The audit used the actual implementations in `admin/src/components/`:

- `Button(options, ...content)` returns an `HTMLButtonElement`. It supports `label`, `variant`, `size`, `type`, `disabled`, `onClick`, `className`, and safe `attrs`. It cannot render an anchor or accept `href`.
- `Tabs({tabs, activeTab, accessibleName, onChange, onChangeWithDirection, className, idPrefix})` creates the tablist, buttons, keyboard behavior, active state, and `aria-controls`.
- `TabPanel({content, direction, className, id, hidden})` creates one panel. `TabbedPanel` renders one active panel dynamically; neither provides a persistent multi-panel manager.
- `PageHeader({title, description, actions, className, variant})` creates the standard route header with an `h1`. It has no `breadcrumbs` or configurable heading level.
- `FilterBar({groups, className})` and `FilterGroup({label, forId, control, className})` own the filter wrapper and label structure.
- `SearchControl({id, value, label, placeholder, debounce, onQueryChange, ...})` provides search input, clear button, debounce, and cleanup.
- `SelectControl({id, value, options, onChange, ...})` supports normalized option objects, legacy `<option>` arguments, and `.updateOptions(...)`.
- `Modal.render(content, actions, tabs)` expects DOM nodes for `actions`, not `{label, variant, onClick}` configuration objects.
- `Table({columns, ...}).render(data, rowRenderer)` owns the table structure; row renderers should continue to own domain-specific cells.
- `TableRegionPreset(...)` composes `Table`, `DataRegion`, skeletons, errors, and keyed row reconciliation.
- `Pagination({page, totalPages, totalItems, onPageChange, ...})` owns pagination buttons and summary.
- `EmptyState(icon, title, description, action, variant)` covers standardized empty screens.
- `ErrorState(messageOrOptions, onRetry)` covers error text and retry buttons.
- `Notice({message, detail, variant, alert, className})` covers one-line notices, but does not accept arbitrary child content.
- `SpinnerWithText({size, text, className})` covers spinner-plus-label loading states.
- `Badge({text, variant, size, pill, dot, pulse, ...})` covers status labels and indicators.
- `SurfaceCard({children, className, ariaLabel})` covers arbitrary card shells.
- `StatCard({icon, label, value, growth, variant, trend, comparison})` covers metric cards.
- `ActionGroup(children, className)` covers repeated action wrappers.
- `FormControlGroup({label, forId, control, help, className})`, `InputControl`, and `TextareaControl` cover low-level form markup.
- `TranslationProgressView`, `TranslationProgressCompletion`, and `ProgressBar` cover standard translation progress and completion shells.
- `BulkActionBar` is specialized: its actions are hard-coded to Generate Selected, Delete Translations, and Clear Selection. It does not support arbitrary actions or a target-language selector.

Important gaps are discussed below: no anchor-capable `Button`, no persistent tab-panel manager, no `DetailList`, no standardized translation-result list, and no generic batch queue component.

## 2. Per-file findings

### `admin/src/editor/editor-state.mjs`

No UI. Pure block-editor state helpers. No refactor required.

### `admin/src/editor/translation-state.mjs`

No UI. Pure translation polling/state helpers. No refactor required.

### `admin/src/editor/translation-metabox.js`

#### 1. Loading, empty, and dynamic notice text — approximately L190–214

- Inline UI: manually managed `<p>` elements for loading, auto-draft, no-targets, and “save first” notices.
- Replacement: `SpinnerWithText({text})` for loading; `EmptyState(icon, title, description)` for no targets; `Notice({message, variant, alert, className})` for save-first/error messaging. The existing status row already correctly uses `Badge`, `Button`, and `Notice`.
- Difficulty: Moderate.
- Coupling risk: `applyDirtyState()`, `captureFocus()`, and `restoreFocus()` query editor-specific classes and depend on stable target-row controls. Replacing nodes must preserve focus and live-region behavior.

#### 2. Styled edit anchor — approximately L281–287

- Inline UI: raw `<a>` styled with button classes.
- Replacement: No existing shared component fully covers it. `Button` cannot render `href`, `target`, or `rel`.
- Difficulty: Moderate.
- Coupling risk: editor-specific focus restoration queries `.ipz-editor-translation__edit`; a new anchor component must preserve that class and the `data-language` row relationship.

### `admin/src/main.js`

No inline component duplication.

`AppFrame`, `SkeletonPage`, `ErrorState`, and `Toast` are used correctly. The raw route `<div>` created around L317 is only a lifecycle host, not a visual component.

Minor non-component note: `ROUTE_SKELETONS` describes seven translation tabs while `TranslationsPage` currently renders six.

### `admin/src/pages/TeamDashboard.js`

#### Raw route heading — approximately L24–26

- Inline UI: raw `section > h1`.
- Replacement: `PageHeader({title: 'Team Dashboard', description, ...})`.
- Difficulty: Trivial.
- Coupling risk: preserve `section#ipz-team-root`, because route skeleton selection and router ownership depend on that ID.

`SurfaceCard`, `DataRegion`, and `Skeleton` are already used correctly. Member headings and metadata are domain content and should remain custom.

### `admin/src/pages/WorkflowDashboard.js`

#### Raw route heading — approximately L24–26

- Inline UI: raw `section > h1`.
- Replacement: `PageHeader({title: 'Workflow Dashboard', description, ...})`.
- Difficulty: Trivial.
- Coupling risk: preserve `section#ipz-workflow-root` for route skeleton and lifecycle behavior.

`SurfaceCard`, `DataRegion`, and `Skeleton` are already used correctly.

### `admin/src/pages/account.js`

Clean.

`PageHeader`, `Button`, `EmptyState`, `ErrorState`, `SkeletonTable`, and `Table` are used appropriately. The account table is intentional domain data, not a competing table implementation.

### `admin/src/pages/analytics.js`

Clean.

This file delegates entirely to the shared `AnalyticsDashboard` section.

### `admin/src/pages/content-translate.js`

#### 1. Site-entry card shells — approximately L424–448, L491–508

- Inline UI: custom card wrappers using `div.ipz-site-entry__card`.
- Replacement: `SurfaceCard({children, className: 'ipz-site-entry__card'})` can preserve the custom class while providing the shared `ipz-card` shell.
- Difficulty: Trivial to moderate.
- Coupling risk: existing CSS may depend on the current element type and child structure.

#### 2. Repeated definition-list details — approximately L427–438 and L2028–2041

- Inline UI: manually assembled `<dl>`, `<dt>`, and `<dd>` detail lists.
- Replacement: No existing `DetailList` component. `Table` is not a good semantic replacement.
- Difficulty: Moderate.
- Coupling risk: values include mixed text and nodes, and the editor-guidance list is embedded in modal content.
- Candidate: new `DetailList({items, className})`.

#### 3. Site-entry loading text — approximately L474–476

- Inline UI: raw loading paragraph.
- Replacement: `SpinnerWithText({text: 'Loading site entry pages…'})` or a shaped `Skeleton`.
- Difficulty: Trivial.
- Coupling risk: `renderSiteEntrySummary()` clears and repaints the host; preserve the loading host and async lifecycle.

#### 4. Status and action controls — approximately L325–363

- Inline UI: raw `<strong>` status text, disabled translating button, and raw action buttons.
- Replacement: `Badge({text, variant})` for state; `Button({label, variant, size, disabled, attrs, onClick})` for translating/action controls.
- Difficulty: Trivial to moderate.
- Coupling risk: `data-focus-key`, `aria-label`, disabled state, and submission maps must survive rerenders.

#### 5. Persistent custom-type tab panels — approximately L645–714

- Inline UI: `Tabs` is shared, but panel elements, `role="tabpanel"`, `aria-labelledby`, hidden state, and panel relocation are manually implemented.
- Replacement: `TabPanel({content, id, hidden})` can cover each panel’s markup. `Tabs` already covers the tab buttons and keyboard behavior.
- API gap: no shared persistent `TabPanelSet`; `TabbedPanel` renders only one active panel.
- Difficulty: Hard.
- Coupling risk: `typeContentRoot` is physically moved between panels, and IDs must continue matching `Tabs`’ generated `aria-controls`.

#### 6. Filter action wrapper — approximately L810–825

- Inline UI: raw `.ipz-filter-group--actions` containing Clear Filters and Generate All buttons.
- Replacement: `ActionGroup([Button(...), Button(...)], 'ipz-filter-group--actions')`, or a labeled `FilterGroup` if the filter layout requires it.
- Difficulty: Trivial.
- Coupling risk: existing filter CSS and Generate All button lookup by ID.

#### 7. Generate-All language rows — approximately L1263–1288

- Inline UI: clickable `div` rows containing a shared `Checkbox`, flag, language name, count, and custom selected/done behavior.
- Replacement: `SelectionCard({id, index, title, count, selected, onSelect, className, attrs})` is close but cannot render the flag, checkbox, or custom “fully translated” state.
- API gap: requires extending `SelectionCard` or introducing a language-choice component.
- Difficulty: Moderate.
- Coupling risk: click-versus-checkbox event handling and modal-wide `_generateAllSelectedLangs` state.

#### 8. Translation completion result rows — approximately L1219–1238 and L2661–2691

- Inline UI: repeated result `<div>` rows with raw success/failure symbols, status classes, errors, and edit links.
- Replacement: `TranslationProgressCompletion(...)` already correctly covers the outer completion shell. `Badge` can cover statuses, but no shared result-list component exists.
- Difficulty: Moderate.
- Coupling risk: modal content is rerendered while progress state and result arrays are changing; links also require the missing anchor component.

#### 9. Table action wrapper — approximately L1525–1544

- Inline UI: raw `.ipz-table-actions` wrapper around shared Buttons.
- Replacement: `ActionGroup([Button(...), Button(...)], 'ipz-table-actions')`.
- Difficulty: Trivial.
- Coupling risk: row click propagation and keyed `TableRegionPreset` reconciliation.

#### 10. Failure field notice — approximately L1854–1857

- Inline UI: raw alert paragraph for failed translation text.
- Replacement: `Notice({message, variant: 'error', alert: true, className: 'ipz-field-error'})`.
- Difficulty: Trivial.
- Coupling risk: current CSS targets field-level error styling rather than the standard notice layout.

#### 11. WordPress editor links — approximately L1979–1986 and L2673–2680

- Inline UI: raw external anchors.
- Replacement: no current component. Add/use an anchor-capable `LinkButton`.
- Difficulty: Moderate.
- Coupling risk: modal rerendering, target post IDs, `target="_blank"`, and existing `.ipz-ct-edit-link__anchor` styling.

The page already uses `FilterBar`, `BulkActionBar`, `Checkbox`, `Badge`, `TableRegionPreset`, `Pagination`, `Modal`, `Disclosure`, `TextareaControl`, `InputControl`, and translation-progress components correctly.

### `admin/src/pages/dashboard.js`

#### 1. Quick-action wrapper — approximately L231–238

- Inline UI: raw action-button container around three shared Buttons.
- Replacement: `ActionGroup([...], 'ipz-dashboard__action-buttons')`.
- Difficulty: Trivial.
- Coupling risk: dashboard-specific spacing CSS.

#### 2. Raw job status badge — approximately L247–250

- Inline UI: manually assembled legacy badge classes.
- Replacement: `new Badge({text: statusInfo.text, variant: statusInfo.variant})`.
- Difficulty: Trivial.
- Coupling risk: legacy `.presszone-international-badge` selectors may need CSS migration.

#### 3. Job title anchor — approximately L251–257

- Inline UI: styled raw internal anchor.
- Replacement: no current anchor-capable Button. Use the proposed `LinkButton`, or leave as a normal link if the visual treatment is intentionally link-like.
- Difficulty: Moderate.
- Coupling risk: the URL points to a dashboard route that is not otherwise represented by a shared navigation component.

`PageHeader`, `Card`, `StatRegionPreset`, `TableRegionPreset`, and `emptyStateRow` are already correct.

### `admin/src/pages/fields-translate.js`

#### 1. DataRegion loading placeholder — approximately L93–98

- Inline UI: raw loading paragraph.
- Replacement: `SpinnerWithText({text})` or an appropriate `Skeleton`.
- Difficulty: Trivial.
- Coupling risk: DataRegion skeleton callback must continue to return a stable host.

#### 2. Table empty row — approximately L520–534

- Inline UI: raw `<tr><td colspan="5">…`.
- Replacement: `emptyStateRow(5, {title, description, icon})` from `RegionPresets`.
- Difficulty: Trivial.
- Coupling risk: preserve the table’s caption, column count, and row-region semantics.

The rest of this page is a strong shared-component example: `SearchControl`, `FormControlGroup`, `SelectionCard`, `Disclosure`, `Table`, `Badge`, `Pagination`, `EmptyState`, `Notice`, and `Button` are already used correctly.

### `admin/src/pages/languages.js`

#### Action wrapper — approximately L270–292

- Inline UI: raw `.ipz-actions-group` around shared row Buttons.
- Replacement: `ActionGroup([...], 'ipz-actions-group')`.
- Difficulty: Trivial.
- Coupling risk: row-level DataRegion reconciliation and custom action-group CSS.

The table, empty state, sorting Button, ToggleSwitch, Badge, Modal, Select, FormField, and Toggle usage is already correct.

### `admin/src/pages/migration.js`

#### 1. Migration step indicator — approximately L59–85

- Inline UI: hand-rolled five-step indicator with active/completed classes, numbers, and labels.
- Replacement: no existing stepper component. `Tabs` is not an appropriate replacement because the steps are not user-selectable.
- Difficulty: Moderate.
- Coupling risk: step state is coupled to `this.step`, CSS class names, and rerendering the whole wizard.

#### 2. WordPress-style notices — approximately L92–99, L219–229, and L377–392

- Inline UI: raw `notice notice-error`, `notice notice-success`, and conditional verification notices.
- Replacement: `Notice({message, variant: 'error'|'success', alert: true})`.
- Difficulty: Trivial.
- Coupling risk: existing migration CSS may target WordPress notice classes; backup success also has a second paragraph that must remain outside the Notice.

#### 3. Duplicate spinner and loading text — approximately L230–236

- Inline UI: shared `Spinner` plus a duplicated raw paragraph containing the same message.
- Replacement: `SpinnerWithText({text: 'Creating backup...'})`.
- Difficulty: Trivial.
- Coupling risk: Card body layout and migration loading CSS.

#### 4. Repeated action wrappers — approximately L192–204, L250–290, L341–354, and L394–405

- Inline UI: four hand-built `.ipz-actions` containers.
- Replacement: `ActionGroup(children, 'ipz-actions')`.
- Difficulty: Trivial.
- Coupling risk: shared spacing selectors and step navigation callbacks.

#### 5. Preflight checklist indicators — approximately L171–190

- Inline UI: raw `<ul>/<li>` rows and dashicon spans for passed/failed/warning states.
- Replacement: retain the list because there is no checklist component, but replace the indicator spans with `Badge` or `StatusFeedback`.
- Difficulty: Moderate.
- Coupling risk: check severity mapping and the custom `.ipz-check-*` CSS contract.

#### 6. Completion success panel — approximately L457–470

- Inline UI: raw dashicon span, heading, and paragraph.
- Replacement: `SuccessCheckmark()` or `StatusFeedback({type: 'success', children})` for the indicator. `Card` already correctly owns the outer panel.
- Difficulty: Trivial to moderate.
- Coupling risk: existing `.ipz-success-message` and dashicon styling.

### `admin/src/pages/my-assignments.js`

#### 1. Raw route header — approximately L61–72

- Inline UI: manually assembled `.ipz-page-header` with title and description.
- Replacement: `PageHeader({title, description})`.
- Difficulty: Trivial.
- Coupling risk: preserve the route container ID and assignment-specific layout.

#### 2. Persistent tab panels — approximately L89–128

- Inline UI: shared `Tabs`, but manual panel creation, `aria-labelledby`, hidden state, panel map, and switching.
- Replacement: `TabPanel` can create each panel; no existing component manages the persistent panel map.
- Difficulty: Moderate.
- Coupling risk: panel identity is coupled to `currentTab`, DataRegion mounting, and asynchronous `loadAssignments()` calls.

#### 3. Assignment detail definition list — approximately L298–316

- Inline UI: raw `<dl>/<dt>/<dd>` detail list.
- Replacement: no existing component. Candidate `DetailList`.
- Difficulty: Moderate.
- Coupling risk: modal content and workflow state are assembled together.

#### 4. Extension-request form groups — approximately L359–381

- Inline UI: raw `.ipz-form-group` wrappers and labels around already-shared `InputControl` and `TextareaControl`.
- Replacement: `FormControlGroup({label, forId, control, className: 'ipz-form-group'})`.
- Difficulty: Trivial.
- Coupling risk: modal field IDs and local `newDeadline`/`reason` state.

#### 5. Invalid Modal action configuration — approximately L383–406

- Inline UI/API issue: `Modal.render(content, [{label, variant, onClick}, ...])` passes plain objects as actions.
- Replacement: create actual `Button({...})` nodes before passing them to `Modal.render`.
- Difficulty: Trivial.
- Coupling risk: High. `Modal` calls `appendChild` on actions, so these objects can fail at runtime rather than render as buttons.

`AssignmentCard`, `ListRegionPreset`, `DataRegion`, `Skeleton`, `WorkflowTimeline`, and `Tabs` are otherwise appropriate.

### `admin/src/pages/onboarding.js`

Clean.

`PageHeader`, `Button`, `ErrorState`, and `SpinnerWithText` are used correctly. The package choices are domain-specific actions rather than a clear `SelectionCard` use case because clicking immediately begins the connection flow.

### `admin/src/pages/settings.js`

#### 1. Cache statistics grid — approximately L401–425

- Inline UI: four hand-built `.stat-item` blocks for hit ratio, cache size, hits, and misses.
- Replacement: four `StatCard({icon, label, value, variant})` instances, or a shared stat-grid preset.
- Difficulty: Moderate.
- Coupling risk: existing `.cache-stats`, `.stats-grid`, `.stat-item`, `.stat-label`, and `.stat-value` CSS.

#### 2. Modal actions placed inside content — approximately L647–727

- Inline UI: two confirmation modals each place Buttons inside a manually nested `.modal-actions` content wrapper.
- Replacement: `modal.render(contentParagraph, [CancelButton(...), Button(...)])`. `Modal` already creates the standard footer.
- Difficulty: Trivial.
- Coupling risk: existing `.modal-actions` CSS and asynchronous callbacks that close the modal after mutation.

The settings tabs, `TabPanel`, form fields, Selects, Toggles, ExceptionsTab, Buttons, and Toasts are already correct.

### `admin/src/pages/site-content.js`

#### 1. Raw page header — approximately L74–79

- Inline UI: raw `.ipz-page-header` and `h1`.
- Replacement: `PageHeader({title: 'Site Content'})`.
- Difficulty: Trivial.
- Coupling risk: custom `ipz-site-content` root and existing header CSS.

#### 2. Hand-rolled tab bar and panel semantics — approximately L87–149 and L151–157

- Inline UI: raw tablist, buttons, active classes, keyboard navigation, `aria-controls`, and a single manually updated panel.
- Replacement: `Tabs({tabs, activeTab, accessibleName, onChange, className: 'presszone-multilingual-tabs', idPrefix: 'site-content'})`.
- `TabPanel` can cover the panel markup, but the current one-panel/multi-tab relationship requires ID changes or a small persistent-panel adapter.
- Difficulty: Moderate.
- Coupling risk: `this.tabs`, `this.searchInput`, `this.panel`, focus restoration, and `aria-labelledby` are directly coupled to current IDs.

#### 3. Raw search field — approximately L95–103

- Inline UI: manually nested label and `<input type="search">`, with a custom debounce timer.
- Replacement: `FilterBar` + `FilterGroup` + `SearchControl({id, value, label, placeholder, debounce, onQueryChange})`.
- Difficulty: Moderate.
- Coupling risk: `scheduleSearch()`, `this.searchInput`, and focus restoration need to use `SearchControl.input`.

#### 4. Loading state — approximately L251–256

- Inline UI: raw loading paragraph.
- Replacement: `SpinnerWithText({text})`.
- Difficulty: Trivial.
- Coupling risk: preserve `aria-busy` on the panel.

#### 5. Error and retry state — approximately L258–266

- Inline UI: raw `.ipz-notice--error` wrapper and raw retry button.
- Replacement: `ErrorState({message, onRetry, className: 'ipz-site-content__error'})`.
- Difficulty: Trivial.
- Coupling risk: current code restores the panel’s `aria-busy` state and focus after retry.

#### 6. Empty state and pagination — approximately L269–295

- Inline UI: raw empty paragraph plus manually assembled previous/next navigation and summary.
- Replacement: `EmptyState(icon, title, description)` and `Pagination({page, totalPages, totalItems, showPageSize: false, summary, onPageChange})`.
- Difficulty: Moderate.
- Coupling risk: page boundaries, total summary text, and focus restoration.

#### 7. Site-content card shell — approximately L317–321

- Inline UI: raw `article.ipz-card`.
- Replacement: `SurfaceCard({children, className: 'ipz-site-content__item'})`.
- Difficulty: Trivial.
- Coupling risk: preserve the existing custom class and article semantics.

#### 8. Translation status/action controls — approximately L324–363

- Inline UI: raw status `<strong>`, disabled translating button, and multiple raw action buttons.
- Replacement: `Badge` for status; `Button({label, variant, disabled, attrs, onClick})` for actions.
- Difficulty: Moderate.
- Coupling risk: `data-focus-key`, submission state, `aria-busy`, and post-rerender focus restoration.

#### 9. Review form controls and buttons — approximately L451–482

- Inline UI: raw textareas, labels, Cancel button, and submit button.
- Replacement: `TextareaControl({id, rows, value, attrs, readOnly})`, `FormControlGroup`, and `Button({type: 'submit', ...})`.
- Difficulty: Moderate.
- Coupling risk: segment IDs/source hashes, draft collection, form submission, and focus restoration.

#### 10. Changed-segment recovery block — approximately L467–477

- Inline UI: custom warning `<section>` containing additional read-only textareas.
- Replacement: `Notice` can cover the heading/message only. It cannot accept arbitrary child fields, so the recovery field list must remain custom or be supported by a new composable notice API.
- Difficulty: Moderate.
- Coupling risk: recovery values are intentionally preserved outside the editable segment list.

### `admin/src/pages/string-translate.js`

#### 1. Filter action wrapper — approximately L454–475

- Inline UI: raw `.ipz-filter-group--actions` wrapper around Refresh Scan and Generate All Buttons.
- Replacement: `ActionGroup([...], 'ipz-filter-group--actions')`.
- Difficulty: Trivial.
- Coupling risk: scan/generate button IDs and status updates.

The main filter groups already use `FilterBar`, `FilterGroup`, `SearchControl`, and `SelectControl` correctly. The custom focused-select patching logic should not be replaced blindly: `SelectControl.updateOptions()` reuses option nodes, but the page also defers updates while a select is focused.

#### 2. Table action wrapper — approximately L647–668

- Inline UI: raw `.ipz-table-actions`.
- Replacement: `ActionGroup([...], 'ipz-table-actions')`.
- Difficulty: Trivial.
- Coupling risk: row click propagation and DataRegion patching.

#### 3. Generate-All language rows — approximately L1563–1596

- Inline UI: custom clickable language row containing `Checkbox`, flag, name, and count.
- Replacement: `SelectionCard` is not an exact fit because its actual API is a button with fixed number/title/count/chevron content, not a flag-plus-checkbox row.
- Difficulty: Moderate.
- Coupling risk: checkbox event suppression and `_generateAllSelectedLangs` state.

This is a duplicate of the same language-row implementation in `content-translate.js`.

#### 4. Translation result rows — approximately L1366–1387 and L1514–1544

- Inline UI: raw result rows with success/failure symbols, language labels, errors, and counts.
- Replacement: `TranslationProgressCompletion` already covers the outer completion component. `Badge` can cover status, but there is no shared result-list/item component.
- Difficulty: Moderate.
- Coupling risk: modal rerenders, per-language result state, and shared styling classes.

The page’s `BulkActionBar`, `TableRegionPreset`, `Badge`, `Checkbox`, `Pagination`, `Modal`, and translation-progress components are otherwise used correctly.

### `admin/src/pages/translate-history.js`

#### 1. Hand-rolled filter bar — approximately L229–311

- Inline UI: raw `.ipz-filters`, `.ipz-filters__row`, filter groups, labels, and manually populated SelectControl options.
- Replacement: `FilterBar({groups})` with `FilterGroup`, existing `SearchControl`, and `SelectControl({options, onChange})`.
- Difficulty: Moderate.
- Coupling risk: store remounts, `this.filters`, search-control cleanup, and current filter CSS.

The current `SelectControl` itself is shared; the issue is the duplicated wrapper and option-construction pattern.

#### 2. Row action wrappers — approximately L413–439 and L813–832

- Inline UI: raw `.ipz-table-actions` containers in initial rows and live-patched rows.
- Replacement: `ActionGroup(children, 'ipz-table-actions')`.
- Difficulty: Trivial.
- Coupling risk: live polling patches visible rows directly and must preserve button event handlers.

#### 3. Modal detail definition lists — approximately L529–585

- Inline UI: repeated raw `<dl>/<dt>/<dd>` sections.
- Replacement: no existing component. Candidate `DetailList`.
- Difficulty: Moderate.
- Coupling risk: mixed Badge/link values and conditional error sections.

#### 4. Modal post/view links — approximately L504–523

- Inline UI: raw Edit/View anchors.
- Replacement: proposed `LinkButton` or a link-list primitive.
- Difficulty: Moderate.
- Coupling risk: external URLs, target/rel attributes, and modal-specific classes.

#### 5. Modal footer wrapper — approximately L598–608

- Inline UI: raw `.ipz-table-actions` wrapper passed as one Modal action node.
- Replacement: pass the shared Button directly, or use `ActionGroup` only if multiple actions are required.
- Difficulty: Trivial.
- Coupling risk: Modal already supplies the footer, so changing this may affect footer spacing CSS.

API note: `PageHeader` receives `breadcrumbs` around L217–223, but the actual `PageHeader` API ignores that property.

### `admin/src/pages/translate-posts.js`

#### 1. Hand-rolled filter bar — approximately L334–446

- Inline UI: raw filter row/groups/labels and manually assembled source, completed, and missing-language Selects.
- Replacement: `FilterBar` + `FilterGroup` + existing `SearchControl`/`SelectControl`. Use `SelectControl({options, value, onChange})` and `.updateOptions(...)`.
- Difficulty: Moderate.
- Coupling risk: `state.filters`, `filterContainer`, language-count updates, and select focus state.

#### 2. Generating spinner — approximately L738–747

- Inline UI: raw spinner div plus generating paragraph.
- Replacement: `SpinnerWithText({text})` or `IndeterminateTranslationProgress(...)`.
- Difficulty: Trivial.
- Coupling risk: modal content layout and translation status rerenders.

#### 3. Styled anchors — approximately L763–799, L862–890, L932–937, and L1131–1137

- Inline UI: repeated raw Edit Translation/Edit links styled with Button classes.
- Replacement: no existing component; use the proposed `LinkButton`.
- Difficulty: Moderate.
- Coupling risk: modal action arrays, external targets, post edit URLs, and result-row CSS.

#### 4. Table action wrapper — approximately L924–939

- Inline UI: raw `.ipz-table-actions` around Translate Button and Edit anchor.
- Replacement: `ActionGroup([...], 'ipz-table-actions')` plus `LinkButton`.
- Difficulty: Trivial once `LinkButton` exists.
- Coupling risk: row-level click behavior and anchor/button visual alignment.

#### 5. Translate-All result rows — approximately L1120–1147

- Inline UI: raw result rows with status symbols, errors, and Edit links.
- Replacement: `TranslationProgressCompletion` already covers the outer component; use a shared result-list/item component, `Badge`, and `LinkButton`.
- Difficulty: Moderate.
- Coupling risk: modal tab state and asynchronous progress completion.

#### 6. Custom bulk action bar — approximately L1245–1311

- Inline UI: competing bulk bar with selected count, estimated characters, target-language Select, Translate Selected, and Clear buttons.
- Replacement: existing `BulkActionBar` does not cover this use case. Its constructor only supports Generate Selected/Delete Translations/Clear Selection and its own confirmation modal.
- API gap: generalize `BulkActionBar` with renderable actions/children, or introduce a separate `SelectionActionBar`.
- Difficulty: Hard.
- Coupling risk: high. It is coupled to `selectedPosts`, `bulkTargetLang`, language availability, character estimates, button IDs, and bulk modal state.

#### 7. Bulk queue progress surface — approximately L1436–1548

- Inline UI: custom queue header, count, ETA, raw progress-bar host, queue item list, status symbols, and direct DOM updates through `data-bulk-*` selectors.
- Replacement: `TranslationProgressView` can cover the standard progress shell; `ProgressBar({container, value, max, ...})` can replace the raw bar and `.setValue(...)` can replace `setProgressValue`. No existing component covers the ETA-plus-item queue.
- Difficulty: Hard.
- Coupling risk: high. `bulkProgress`, scroll positioning, `data-queue-index`, polling-like updates, and modal DOM queries are all intertwined.

#### 8. Bulk completion surface — approximately L1550–1633

- Inline UI: custom completion icon/header, stats spans, and result list.
- Replacement: `TranslationProgressCompletion({title, icon, failed, children, baseClass, rootClass})` directly covers the completion shell. `StatCard` may cover the metrics if the visual treatment is acceptable; a result-list component is still missing.
- Difficulty: Moderate to hard.
- Coupling risk: completion state, early-stop state, duration calculations, and modal footer updates.

API notes:

- `PageHeader` receives unsupported `breadcrumbs` around L100–106.
- `Tabs` receives `href` fields around L115–119, but the actual `Tabs` API ignores `href`; navigation currently depends on `onChange`.

The page’s `Table`, `Checkbox`, `Badge`, `Pagination`, `Modal`, `ProgressBar`, and translation-progress components are otherwise correctly used.

### `admin/src/pages/translations.js`

#### Persistent tab panels — approximately L227–260

- Inline UI: manual creation and activation of six persistent `role="tabpanel"` elements.
- Replacement: `TabPanel` can cover each individual panel’s markup. The current `TabbedPanel` cannot preserve six panels and their subpage lifecycle state.
- API gap: persistent tab-panel manager.
- Difficulty: Moderate to hard.
- Coupling risk: very high. Each panel owns a subpage, async rendering generation, route state, and hidden/active state.

The shared `Tabs` component itself is correctly used.

### `admin/src/sections/analytics-dashboard.js`

#### 1. Unavailable/error panel states — approximately L73–94

- Inline UI: raw muted paragraphs and retry Button inside custom unavailable wrappers.
- Replacement: `Notice({message, variant: 'warning', className})` for unavailable information; `ErrorState({message, onRetry, className})` for retryable failures.
- Difficulty: Trivial to moderate.
- Coupling risk: DataRegion error callback signature and analytics-specific sizing classes.

#### 2. Insight metric cards — approximately L185–197

- Inline UI: two manually assembled label/value metric blocks.
- Replacement: `StatCard({icon, label, value, variant})`.
- Difficulty: Moderate.
- Coupling risk: current `.ipz-analytics-insight` CSS and dashboard grid sizing.

#### 3. Recommendation list — approximately L199–210

- Inline UI: custom `<ul>/<li>` list and empty paragraph.
- Replacement: no existing shared list component is a good semantic fit. `SortableList` and `WorkflowTimeline` are inappropriate here.
- Difficulty: Not recommended without a new generic ranked-list component.
- Coupling risk: analytics-specific score formatting and recommendation layout.

#### 4. Dashboard section header — approximately L436–451

- Inline UI: manually assembled `h2`, description, action wrapper, and Refresh Button.
- Replacement: `PageHeader` can render title, description, and actions, but always renders an `h1` and route-header markup.
- API gap: a `SectionHeader({title, description, actions, headingLevel: 2})` or configurable `AppFrameHeader`.
- Difficulty: Moderate.
- Coupling risk: this is specifically a dashboard section, not a route header; changing heading level or CSS could affect document hierarchy.

#### 5. Export links — approximately L477–486, invoked three times

- Inline UI: raw anchors styled as Buttons.
- Replacement: no current anchor-capable component; use `LinkButton`.
- Difficulty: Moderate.
- Coupling risk: generated URLs, new-tab behavior, export nonce, and card action CSS.

`Card`, `DataRegion`, `TableRegionPreset`, `emptyStateRow`, skeletons, `PageHeader`, and Buttons are otherwise correctly used.

## 3. Competing or duplicate implementations

| Pattern | Competing implementations |
|---|---|
| Page headers | `TeamDashboard`, `WorkflowDashboard`, `my-assignments`, and `site-content` hand-roll headers while most other routes use `PageHeader`. |
| Persistent tab panels | `translations`, `content-translate`, `my-assignments`, and `site-content` manually manage panels around the shared `Tabs` component. |
| Filter bars | `translate-history` and `translate-posts` manually assemble filter rows, while `content-translate` and `string-translate` use `FilterBar`. |
| Styled link-buttons | Editor, dashboard, analytics, history, content translation, and Translate Posts use raw anchors with Button classes. |
| Action wrappers | `.ipz-table-actions`, `.ipz-filter-group--actions`, `.ipz-actions`, and dashboard-specific wrappers repeat the same layout role despite `ActionGroup`. |
| Translation result rows | `content-translate`, `string-translate`, and `translate-posts` each implement result rows with status symbols, language labels, errors, and links. |
| Language-selection rows | `content-translate` and `string-translate` duplicate the clickable flag/checkbox/count row pattern. |
| Bulk action bars | `translate-posts` has a custom Translate Selected bar, while `content-translate` and `string-translate` use the specialized shared `BulkActionBar`. |
| Detail lists | `content-translate`, `translate-history`, and `my-assignments` manually build similar `<dl>` structures. |
| Modal footers | Settings place action Buttons inside modal content; most other pages correctly pass action nodes through `Modal.render`. |

Raw `<tr>`/`<td>` renderers in the table pages are not independently flagged when `Table` or `TableRegionPreset` owns the table structure. Those cells contain page-specific data and are appropriate to keep custom.

## 4. New component candidates

These patterns occur at least three times and have no complete existing component equivalent.

### `LinkButton`

Repeated raw styled anchors occur in the editor metabox, dashboard, analytics export cards, translation history, content translation, and Translate Posts.

Suggested API:

```js
LinkButton({
    label,
    href,
    variant,
    size,
    target,
    rel,
    className,
    attrs
})
```

It should emit the same visual classes as `Button` while retaining native anchor semantics.

### `PersistentTabSet`

The same tab-plus-persistent-panel manager appears in `translations`, `content-translate`, `my-assignments`, and `site-content`.

Suggested API:

```js
PersistentTabSet({
    tabs,
    activeTab,
    accessibleName,
    renderPanel,
    preservePanels: true,
    onChange,
    className,
    idPrefix
})
```

It should compose `Tabs` and `TabPanel`, maintain stable panel nodes, and expose the active panel without forcing pages to duplicate hidden-state and ARIA management.

### `TranslationResultList`

The same completion/result rows occur across content translation, string translation, and Translate Posts.

Suggested API:

```js
TranslationResultList({
    results,
    getLabel,
    getStatus,
    getError,
    getLink,
    className
})
```

It should compose `Badge` and the proposed `LinkButton`.

### `DetailList`

Similar semantic `<dl>` markup appears in content translation, translation history, and My Assignments.

Suggested API:

```js
DetailList({
    items: [{ label, value }],
    className,
    caption
})
```

Values should accept strings or DOM nodes so Badges and link groups remain possible.

### Below the three-occurrence threshold

These are worth watching but do not yet meet the requested 3+ threshold:

- `LanguageSelectionRow`: duplicated in `content-translate` and `string-translate`; `SelectionCard` is close but not an exact fit.
- `WizardStepper`: currently only in `migration.js`.
- `BatchProgressQueue`: currently only in `translate-posts.js`.
- `SectionHeader`: currently most visible in the analytics dashboard; the existing `PageHeader` needs a heading-level API if this pattern expands.

## 5. Already clean

Do not touch these files for inline-component refactoring:

- `admin/src/editor/editor-state.mjs`
- `admin/src/editor/translation-state.mjs`
- `admin/src/main.js`
- `admin/src/pages/account.js`
- `admin/src/pages/analytics.js`
- `admin/src/pages/onboarding.js`

The following pages are mostly clean and should preserve their existing shared-component usage; only the specific seams listed above require attention:

- `dashboard.js`: shared PageHeader, Card, StatRegionPreset, TableRegionPreset, and empty-state row.
- `fields-translate.js`: shared form controls, SelectionCard, Disclosure, Table, Pagination, Badge, EmptyState, and Notice.
- `languages.js`: shared TableRegionPreset, empty state, Modal, FormField, Select, Toggle, ToggleSwitch, Badge, and Button.
- `settings.js`: shared PageHeader, Tabs, TabPanel, form controls, ExceptionsTab, Modal, and Toast.
- `content-translate.js`: shared FilterBar, BulkActionBar, TableRegionPreset, Pagination, Checkbox, Badge, Modal, Disclosure, form controls, and translation-progress components.
- `string-translate.js`: shared FilterBar, BulkActionBar, TableRegionPreset, Pagination, Checkbox, Badge, Modal, and translation-progress components.
- `translate-history.js`: shared TableRegionPreset, Pagination, Badge, Button, Modal, SearchControl, and SelectControl.
- `translate-posts.js`: shared Tabs, Table, Pagination, Checkbox, Badge, Modal, Button, and progress components.
- `my-assignments.js`: shared AssignmentCard, ListRegionPreset, DataRegion, Skeleton, WorkflowTimeline, and low-level form controls.

## 6. Suggested independent refactor waves

No file appears in more than one wave.

### Wave 0 — Shared API additions

Files:

- `admin/src/components/LinkButton.js` — new
- `admin/src/components/PersistentTabSet.js` — new
- `admin/src/components/TranslationResultList.js` — new
- `admin/src/components/DetailList.js` — new
- `admin/src/components/index.js`

This wave addresses the current API gaps.

### Wave 1 — Small primitive substitutions

Files:

- `admin/src/editor/translation-metabox.js`
- `admin/src/pages/TeamDashboard.js`
- `admin/src/pages/WorkflowDashboard.js`
- `admin/src/pages/dashboard.js`
- `admin/src/pages/fields-translate.js`
- `admin/src/pages/languages.js`
- `admin/src/pages/migration.js`
- `admin/src/pages/settings.js`

### Wave 2 — Navigation, filters, and modal shells

Files:

- `admin/src/pages/site-content.js`
- `admin/src/pages/translations.js`
- `admin/src/pages/translate-history.js`
- `admin/src/pages/my-assignments.js`

These files share tab-panel, filter, detail-list, and Modal-footer concerns.

### Wave 3 — Translation workflows and batch UI

Files:

- `admin/src/pages/content-translate.js`
- `admin/src/pages/string-translate.js`
- `admin/src/pages/translate-posts.js`

This is the highest-risk wave because it touches polling, DataRegion patching, modal state, selection state, and progress DOM updates.

### Wave 4 — Analytics section cleanup

Files:

- `admin/src/sections/analytics-dashboard.js`

The clean files listed above should remain outside all waves.
