> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# AppIcon Migration Checklist

> > Goal: Move every existing icon call site in src/ over to the AppIcon API > (tone / variant / weight), replacing raw <svg> / direct lucide-react > JSX and har…

> **Goal:** Move every existing icon call site in `src/` over to the `AppIcon` API
> (`tone` / `variant` / `weight`), replacing raw `<svg>` / direct `lucide-react`
> JSX and hard-coded `text-*` color classes with the unified wrapper from
> `src/shared/ui/icon.tsx`.

**Status:** In progress (Phase 0 complete).
**Owner:** Design Systems.
**Cross-links:** [ICON\_GUIDE.md](/development/ICON_GUIDE) · [SEMANTIC\_COLORS.md](/development/SEMANTIC_COLORS) · [`reports/codemod-appicon-tone-dryrun.json`](https://github.com/Encore-OS/encoreos/blob/development/reports/codemod-appicon-tone-dryrun.json)

***

## 1. Scope & baseline

The migration is a **pure UI refactor**. No DB / runtime contract changes, no
PHI/PII implications, no new `AppIcon` props.

### Baseline (captured 2026-04-21)

| Signal                                  |     Count | Source                                                                                                                            |
| --------------------------------------- | --------: | --------------------------------------------------------------------------------------------------------------------------------- |
| `lucide-react` JSX call sites in `src/` | **3,275** | `grep -rE "from 'lucide-react'" src \| wc -l`                                                                                     |
| Existing `<AppIcon>` usages in `src/`   |    **28** | `grep -rE "<AppIcon" src \| wc -l`                                                                                                |
| `<AppIcon>` w/ hard-coded tone class    |     **2** | `grep -rEo "<AppIcon[^>]*text-(success\|warning\|destructive\|info\|muted-foreground\|primary\|accent\|foreground)" src \| wc -l` |

### Lucide imports by bucket

| Bucket                  | Files importing `lucide-react` | Files using `<AppIcon>` |
| ----------------------- | -----------------------------: | ----------------------: |
| `src/shared/ui`         |                             18 |                       1 |
| `src/shared/components` |                             12 |                      10 |
| `src/platform/**`       |                            756 |                       0 |
| `src/cores/**`          |                          2,471 |                       0 |

Re-run the baseline before starting each phase:

```bash theme={null}
grep -rE "from 'lucide-react'" src | wc -l
grep -rE "<AppIcon" src | wc -l
grep -rEo "<AppIcon[^>]*text-(success|warning|destructive|info|muted-foreground|primary|accent|foreground)" src | wc -l
```

***

## 2. Migration phases

Each phase ships as its **own PR** so reviewers can verify visually. Land in order.

### Phase 0 — Tooling guardrails ✅

* [x] ESLint rule `northsight/prefer-appicon-tone` flags hard-coded tone classes on `<AppIcon>`.
* [x] Dry-run codemod report at `reports/codemod-appicon-tone-dryrun.json`.
* [x] Domain-status mapping `domainStatusToIconTone()` in `src/shared/lib/semantic-colors.ts`.
* [x] Solid-variant + weight + tone documented in `docs/development/ICON_GUIDE.md`.

### Phase 1 — `src/shared/ui` and `src/shared/components`

**Target:** 100% AppIcon, 0 hard-coded tone classes.

* [ ] Inventory: `grep -rl "from 'lucide-react'" src/shared/ui src/shared/components`
* [ ] Convert each file per [§4 per-call-site checklist](#4-per-call-site-checklist).
* [ ] Apply codemod findings from `reports/codemod-appicon-tone-dryrun.json` (2 hits today).
* [ ] Re-run dry-run codemod; confirm `byBucket["shared/ui"] === 0` and `byBucket["shared/components"] === 0`.
* [ ] Verification block (see [§5](#5-verification-steps)).

### Phase 2 — `src/platform/**`

**Target:** 95% AppIcon (allow raw Lucide only inside `AppIcon` itself + icon registries like `WIZARD_STEP_ICONS`, `MODULE_ICONS`, `stepIcons`); 0 hard-coded tone classes.

* [ ] Inventory: `grep -rl "from 'lucide-react'" src/platform | wc -l` (baseline: 756).
* [ ] Skip-list: any file matching `*/icons/*`, `*-icons.ts`, `*StepIcons*`, `*ModuleIcons*`, `src/shared/ui/icon.tsx`.
* [ ] Sub-batches by surface area (suggested 100-file chunks):
  * [ ] `src/platform/wizards/**`
  * [ ] `src/platform/forms/**`
  * [ ] `src/platform/table-v2/**`
  * [ ] `src/platform/navigation/**`
  * [ ] `src/platform/dashboard/**`
  * [ ] Remainder.
* [ ] Verification block per sub-batch.

### Phase 3 — `src/cores/**`

**Target:** 90% AppIcon (allow raw Lucide in module-icon configs only); ≤ 5 hard-coded color occurrences.

* [ ] Inventory: `grep -rl "from 'lucide-react'" src/cores | wc -l` (baseline: 2,471).
* [ ] Land per core in this order (smallest blast radius first): `it`, `lo`, `gr`, `fm`, `fw`, `ce`, `rh`, `cl`, `pm`, `fa`, `hr`.
* [ ] Skip-list: `*/icons/*`, `*-icons.ts`, module config maps with `LucideIcon` typed values.
* [ ] Verification block per core.

### Phase 4 — Domain-status surfaces

**Target:** Every status badge / pill / workflow card resolves its tone via `domainStatusToIconTone(status)` instead of an ad-hoc switch.

* [ ] Audit call sites: `grep -rE "tone=\"(success|warning|destructive|info)\"" src` and confirm any **status-driven** tones use the helper.
* [ ] Replace hand-rolled `status === 'approved' ? 'success' : ...` ternaries with `domainStatusToIconTone(status)`.
* [ ] Surfaces to sweep: `StatusBadge`, `EntityStatusPill`, workflow node cards, lead/contact stage chips, billing claim status, encounter status, contract lifecycle.

### Phase 5 — Solid variant sweep

**Target:** Use `variant="solid"` on terminal status badges and hero glyphs.

* [ ] `EmptyState`, `ErrorState`, `SuccessState` hero icons → `variant="solid"`.
* [ ] Toast leading icons (`sonner` custom icons) → `variant="solid"`.
* [ ] Terminal status badges (paid, approved, completed, denied, cancelled) → `variant="solid"`.
* [ ] Inline list-row status (in-flight, draft) stay outline.

### Phase 6 — Weight sweep

**Target:** Establish a weight rhythm.

* [ ] Dense table chrome (sort carets, row affordances, kebab menus) → `weight="thin"`.
* [ ] Primary CTAs (`Button` leading icons) and section headers → `weight="bold"`.
* [ ] Everything else stays default (`regular`).

***

## 3. Coverage targets

| Bucket                            | Approx files | AppIcon target | Hard-coded tone class target | Notes                                                             |
| --------------------------------- | -----------: | -------------: | ---------------------------: | ----------------------------------------------------------------- |
| `src/shared/ui`                   |           18 |       **100%** |                        **0** | Foundation; no exceptions.                                        |
| `src/shared/components`           |           12 |       **100%** |                        **0** | Inc. `EmptyState`, `ErrorState`, `StatusBadge`.                   |
| `src/platform/**`                 |          756 |        **95%** |                        **0** | Allow raw Lucide in icon registries (`stepIcons`, `moduleIcons`). |
| `src/cores/**`                    |        2,471 |        **90%** |                      **≤ 5** | Allow raw Lucide in module-icon configs only.                     |
| `tests/**`, `src/**/__tests__/**` |          n/a |       excluded |                     excluded | Tests stay verbatim.                                              |

**Definition of "AppIcon coverage":**
`(<AppIcon> usages) / (<AppIcon> usages + raw Lucide JSX call sites)` per bucket.
Compute via `grep -rE "<AppIcon" {bucket}` and `grep -rE "<[A-Z][A-Za-z0-9]*\s+[^>]*className=" {bucket}` filtered to identifiers imported from `lucide-react`. Approximation acceptable; the codemod report is the source of truth.

***

## 4. Per-call-site checklist

Apply to every Lucide JSX usage you touch:

1. **Replace JSX**:

   ```tsx theme={null}
   // before
   import { CheckCircle2 } from 'lucide-react';
   <CheckCircle2 className="h-4 w-4 text-success" />

   // after
   import { AppIcon } from '@/shared/ui/icon';
   import { CheckCircle2 } from 'lucide-react';
   <AppIcon icon={CheckCircle2} size="sm" tone="success" />
   ```

2. **Resolve `tone`** from one of:
   * Explicit semantic intent (`success`, `warning`, `destructive`, `info`, `muted`, `neutral`, `primary`, `accent`).
   * `domainStatusToIconTone(status)` for status-driven tones.
   * Context (e.g. `tone="muted"` for dense table chrome).

3. **Resolve `variant`**: `solid` for terminal badges / hero glyphs / toast leads, otherwise default outline.

4. **Resolve `weight`**: `bold` for CTAs / section headers, `thin` for dense table chrome, otherwise default `regular`.

5. **Accessibility**: decorative? leave default (`aria-hidden="true"`). Meaningful (icon-only button)? add `decorative={false}` + `aria-label="..."`.

6. **Drop redundant sizing** (`h-*`, `w-*`) in favor of `size="xs|sm|md|lg"`.

7. **Strip the tone class from `className`** so caller `className` only carries layout/animation utilities.

8. **Re-run lint locally** for the changed file:

   ```bash theme={null}
   npx eslint --fix path/to/changed/file.tsx
   ```

***

## 5. Verification steps

### Per phase / sub-batch

* [ ] `npm run lint` — must pass with **zero new** `northsight/prefer-appicon-tone` warnings in scope.
* [ ] `npm run typecheck` — must pass.
* [ ] `npm run test -- icon` — `AppIcon` unit tests must remain green.
* [ ] Re-run the codemod in dry-run mode and confirm `byBucket` counts for the phase drop to 0:

  ```bash theme={null}
  node /tmp/codemod-appicon-tone.mjs --dry-run > reports/codemod-appicon-tone-dryrun.json
  jq '.byBucket' reports/codemod-appicon-tone-dryrun.json
  ```

### Final acceptance (after Phase 6)

* [ ] `npm run audit:hardcoded-colors` — no new hits on icon className regex.
* [ ] `npm run validate` — full gate green (format + typecheck + lint + build).
* [ ] Visual spot check on **three pages per core** (list, detail, dialog) using the Preview URL.
* [ ] Update `mem://design/v3-visual-identity-and-component-specs` noting the migration is complete.
* [ ] Mark this checklist's phases as `✅`.

***

## 6. Roll-out & rollback

* **Roll-out:** Land per-bucket in separate PRs (Shared → Platform → Cores → Domain-status → Solid → Weight) so reviewers can verify visually.
* **Each PR includes:**
  * Before/after dry-run JSON diff for its bucket (paste relevant `byBucket` slice).
  * Screenshots of 2 representative surfaces (light + dark mode preferred).
  * Lint warning count diff.
* **Rollback:** Each bucket is a pure refactor; revert the PR. No DB / runtime contract changes.

***

## 7. Out of scope

* No new icon tokens, no new `AppIcon` props.
* No changes to icon registries (`WIZARD_STEP_ICONS`, `MODULE_ICONS`, core `stepIcons`).
* No automatic rewrite of raw `<svg>` (manual review only — too rare to script safely).
* No PHI/PII handling implications; pure UI refactor.
* No changes to test files (`tests/**`, `src/**/__tests__/**`).

***

## 8. References

* [`docs/development/ICON_GUIDE.md`](/development/ICON_GUIDE) — usage patterns, tone/variant/weight examples.
* [`docs/development/SEMANTIC_COLORS.md`](/development/SEMANTIC_COLORS) — token catalog and PF-95 theming.
* [`reports/codemod-appicon-tone-dryrun.json`](https://github.com/Encore-OS/encoreos/blob/development/reports/codemod-appicon-tone-dryrun.json) — current codemod findings.
* [`src/shared/ui/icon.tsx`](https://github.com/Encore-OS/encoreos/blob/development/src/shared/ui/icon.tsx) — `AppIcon` source of truth.
* [`src/shared/lib/semantic-colors.ts`](https://github.com/Encore-OS/encoreos/blob/development/src/shared/lib/semantic-colors.ts) — `domainStatusToIconTone()` mapping.
