CarsXE

Search docs

Search pages, components, and skills.

Skills

Carsxe migrate loop

Loop until a consumer app is fully migrated onto @carsxe/design-system: grep leftover UI, migrate a slice, browser screenshot/video QA, repeat until complete.Save as .cursor/skills/carsxe-migrate-loop/SKILL.md. Copy other files in this skill into the same folder.

SKILL.md

markdown
---
name: carsxe-migrate-loop
description: >-
  Loop until a consumer app is fully migrated onto @carsxe/design-system. Grep leftover
  shadcn/local UI, migrate one slice, browser screenshot/video QA, repeat until complete.
  Use when the user wants to migrate an app to the Carsxe design system, run a migrate
  loop, or fully replace local ui/ components with @carsxe/design-system.
---

# Carsxe migrate loop

Same-session greploop: inventory leftover UI → migrate one slice → browser QA → repeat until the app is fully on `@carsxe/design-system`. This is not a timed `/loop` heartbeat.

Migration rules live in the `carsxe-design-system` skill. This skill owns the loop, inventory, and visual proof.

## Inputs

- **App root** (optional): directory to migrate. Default: current working directory.
- Do not commit or push unless the user asked. Keep work in the working tree.

## Instructions

### 0. Prerequisites

Read `carsxe-design-system/SKILL.md` and `reference.md` before changing any UI.

If those files are missing from this skills folder, copy **both** `carsxe-design-system` and `carsxe-migrate-loop` from `packages/skills` (or from the docs catalog) into the agent skills directory, then continue.

Do not confuse `@carsxe/design-system` with `@carsxe/ui`. Do not migrate widget or edit-mode behavior. Do not run `shadcn add`. Do not create wrappers around design-system primitives.

### 1. Bootstrap once

Detect the package manager from lockfiles (`bun.lock` / `bun.lockb` → bun, `pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, else npm).

```bash
bun add @carsxe/design-system
# or: pnpm add @carsxe/design-system
# or: npm install @carsxe/design-system
```

Load CSS in the app entry (layout, `main.tsx`, or global stylesheet):

- Tailwind v4 app → `@carsxe/design-system/globals.css`
- Otherwise → `@carsxe/design-system/styles.css`

Skip reinstall if the package is already a dependency. Still verify the CSS import exists.

### 2. Inventory

Read [inventory.md](inventory.md). Grep the app (exclude `node_modules`, `dist`, `.next`, `.output`, `coverage`, `build`, `.turbo`, `.carsxe-migrate-loop`).

Count **covered** leftover hits. That count is the unresolved-work number for this loop. List uncovered third-party UI separately; it is not a failure.

Record `HITS_BEFORE` at the start of every iteration.

### 3. Loop

Repeat the following cycle. **Max 20 iterations.** Stop early if an iteration does not reduce the hit count.

#### A. Pick a slice

Choose one coherent slice: a single route/page, or one component family (`Button` + usages, `Dialog` + usages). Do not migrate the whole app in one iteration.

Prefer slices that still import from `@/components/ui` or a local `components/ui` folder.

#### B. Migrate the slice

For each leftover primitive in the slice:

1. Swap imports to `@carsxe/design-system/components/<name>` (package root is allowed).
2. Keep `variant`, `size`, `className`, and native element props. Drop wrapper components.
3. Delete local source copies of covered primitives only when nothing else imports them.
4. Keep app-specific composites (page layouts, feature widgets). Those are not design-system primitives.

#### C. Typecheck / lint

If the app has `typecheck` or `lint` scripts, run them on the changed files (or the package). Fix failures caused by this slice before QA.

#### D. Visual QA

Follow [visual-qa.md](visual-qa.md). Screenshot and interact with every route touched this iteration. Record video when the tool supports it. Fail the iteration on visual regressions (rounded `--radius`, wrong tokens/fonts, broken layout or wiring).

#### E. Fix and re-inventory

Fix QA and type failures, then grep again. Set `HITS_AFTER`.

If `HITS_AFTER >= HITS_BEFORE` and covered leftovers remain, **stop** and report blockers. Do not loop without progress.

If covered leftovers remain and the count dropped, go back to **A**.

### 4. Exit conditions

Stop the loop if **any** of these are true:

- **Done:** all of the following hold:
  - Covered inventory hit count is 0
  - CSS import is present
  - No local source copies remain for covered primitives
  - Visual QA passed on every route migrated in this run
  - Typecheck/lint of changed files passed, if those scripts exist
- Max iterations reached (report remaining hits)
- Zero-progress iteration (report blockers)

Uncovered UI (charts, maps, widgets, custom composites) must be listed in the report. It does not block “done”.

After a successful inventory of 0, run a **final full-app visual sweep** of remaining user-facing routes (not only the last slice) per `visual-qa.md`. If that sweep fails, treat it as remaining work and continue the loop if iterations remain.

### 5. Report

```
carsxe-migrate-loop complete.
  Iterations:    N
  Inventory:     0 leftover hits
  Routes QA'd:   N
  Screenshots:   N
  Video:         yes/no
```

If not fully migrated:

```
carsxe-migrate-loop stopped after N iterations.
  Inventory:     M leftover hits
  Routes QA'd:   N
  Screenshots:   N
  Video:         yes/no

Remaining hits:
  - src/components/ui/button.tsx — local primitive copy
  - src/app/settings/page.tsx — import from @/components/ui/dialog

Uncovered (not blocking):
  - src/components/Chart.tsx — third-party chart
```

## Additional resources

- Leftover grep patterns: [inventory.md](inventory.md)
- Screenshot, interaction, and video protocol: [visual-qa.md](visual-qa.md)

inventory.md

markdown
# Leftover UI inventory

Count **covered** hits only. Covered = a primitive that `@carsxe/design-system` ships. Uncovered third-party UI is listed in the report, not counted against done.

## Exclude

Do not search these directories:

```
node_modules
dist
.build
.output
.next
coverage
build
.turbo
.carsxe-migrate-loop
storybook-static
```

Typical ripgrep:

```bash
rg -n --glob '!node_modules/**' --glob '!dist/**' --glob '!.next/**' \
  --glob '!.output/**' --glob '!coverage/**' --glob '!build/**' \
  --glob '!.turbo/**' --glob '!.carsxe-migrate-loop/**' \
  -e '<pattern>'
```

## Covered primitives

These names match `carsxe-design-system/reference.md`:

accordion, alert, alert-dialog, aspect-ratio, attachment, avatar, badge, breadcrumb, bubble, button, button-group, calendar, card, carousel, chart, checkbox, collapsible, combobox, command, context-menu, dialog, direction, drawer, dropdown-menu, empty, field, hover-card, input, input-group, input-otp, item, kbd, label, marker, menubar, message, message-scroller, native-select, navigation-menu, pagination, popover, progress, questionnaire, radio-group, resizable, scroll-area, select, separator, sheet, sidebar, skeleton, slider, sonner, spinner, switch, table, tabs, textarea, toast, toggle, toggle-group, tooltip

`sonner` also appears as `toast` in some shadcn apps. Treat local `sonner.tsx` / `toaster.tsx` that wrap Sonner as covered.

## Patterns to count

### Local shadcn / ui-kit imports

```
from ["']@/components/ui/
from ["']~/components/ui/
from ["']@/components/ui["']
from ["']~/components/ui["']
```

Also count relative imports into a `components/ui/` (or `src/ui/`) folder when the file is a covered primitive, for example:

```
from ["'].*/components/ui/(accordion|alert|alert-dialog|aspect-ratio|attachment|avatar|badge|breadcrumb|bubble|button|button-group|calendar|card|carousel|chart|checkbox|collapsible|combobox|command|context-menu|dialog|direction|drawer|dropdown-menu|empty|field|hover-card|input|input-group|input-otp|item|kbd|label|marker|menubar|message|message-scroller|native-select|navigation-menu|pagination|popover|progress|questionnaire|radio-group|resizable|scroll-area|select|separator|sheet|sidebar|skeleton|slider|sonner|spinner|switch|table|tabs|textarea|toast|toggle|toggle-group|tooltip|toaster)["']
```

### Local source copies

A file is a leftover copy when its path looks like:

```
**/components/ui/<primitive>.tsx
**/components/ui/<primitive>.ts
**/ui/<primitive>.tsx
```

and `<primitive>` is in the covered list (plus `toaster`). Count each such file once, in addition to import hits.

Do **not** count files under `node_modules/@carsxe/design-system`.

### shadcn CLI leftovers aimed at those primitives

Count `shadcn add` / `npx shadcn@latest add` invocations in scripts, README, or comments that add a covered primitive.

A root `components.json` that still aliases `@/components/ui` is a **flag**, not an automatic delete. Note it in the report. Only count it as a hit if the app still resolves covered primitives through that alias.

### Direct headless imports when a DS component exists

Count consumer-app imports of `@radix-ui/*`, `@base-ui/react`, or the direct dependencies used by covered primitives when they duplicate a shipped component.

Do **not** count those imports inside `node_modules`. Do not count app-specific controls whose behavior is materially different from a design-system primitive.

## Do not count (uncovered)

List these in the report under **Uncovered (not blocking)**:

- Maps, editors, and app-specific data grids
- `@carsxe/ui` widgets and edit-mode
- App-specific composites (page shells, feature cards) that compose primitives but are not themselves primitives
- Icon packages (`lucide-react`, and so on)
- `cn()` / `class-variance-authority` utilities in the app, unless they exist only to wrap a covered primitive

## Done check

Covered inventory is 0 when:

1. No import hits for the patterns above
2. No local `components/ui/<primitive>` source files remain for covered primitives
3. No `@radix-ui/*` / `@base-ui/react` hits that duplicate a shipped primitive

After deleting a local primitive, grep once more for that filename so dangling imports are not missed.

visual-qa.md

markdown
# Visual QA

Prove each migrated slice in a real browser. Screenshots are required. Video is required when the tool can record it.

## Tooling

Prefer browser automation the agent already has (Cursor browser / computer-use: navigate, snapshot, screenshot, interact).

If none is available, use Playwright as a **one-off fallback** in this session. Do not add a Playwright CI suite or check in test files unless the app already has them.

Playwright fallback (example):

```bash
npx playwright install chromium
```

Record video and screenshots to `.carsxe-migrate-loop/` (gitignored; do not commit):

```ts
const context = await browser.newContext({
  recordVideo: { dir: ".carsxe-migrate-loop/video" },
})
await page.screenshot({
  path: `.carsxe-migrate-loop/screenshots/${name}.png`,
  fullPage: true,
})
```

If video cannot be recorded, say so in the iteration notes and keep screenshots.

## Dev server

Start or reuse the app’s existing dev command (`dev`, `start`, Vite, Next, TanStack Start). Do not start a second copy on the same port.

Wait until the server is actually serving before opening routes.

## Each iteration

1. Collect every user-facing route touched this slice.
2. Open each route in the browser.
3. If the app has a theme toggle, capture **light** and **`.dark`**.
4. Full-page screenshot of the default state.
5. Screenshot key interactive states that this slice migrated (open dialog, open dropdown, toast visible, tabs switched, select open).
6. Interact with migrated controls: click buttons, type into inputs, open/close overlays. Broken `onClick` / form wiring fails the iteration.
7. Record a short walkthrough video of the slice when recording is available.

Name artifacts:

```
.carsxe-migrate-loop/screenshots/iter-N-<route>-<theme>-<state>.png
.carsxe-migrate-loop/video/iter-N-<route>.webm
```

## Fail the iteration if

- Controls use rounded corners. `--radius` is `0`; corners are sharp. Circular controls (`Switch`, radio, slider thumbs, progress, avatars) may stay `rounded-full`.
- Brand tokens look wrong. Primary should read as `#065774` (teal), not the default shadcn zinc/slate palette.
- Fonts are not the design-system stack: Manrope (UI, body, inputs), DM Sans (headings), DM Mono (code).
- Layout overflow, clipped overlays, or controls that do not open/close.
- Default control height looks compact vs 40px (`h-10`) unless the slice intentionally passed `className` (for example `h-8` on Input).

## Final sweep

When covered inventory hits reach 0, walk remaining user-facing routes (nav links, primary flows), not only the last slice. Screenshot each. Record one walkthrough video of the main flow if video is available.

A failed final sweep is remaining work: fix and continue the loop if iterations remain.

## After QA

Note in the loop report: routes visited, screenshot count, whether video was captured, and any fail reasons. Leave artifacts on disk under `.carsxe-migrate-loop/` for the user; do not paste large binaries into chat.