Skip to main content

shadcn-audit

Use when auditing a RubyUI PR, branch or component against shadcn/ui (Base UI base + vega style + neutral theme): Look, HTML, Props, ARIA, Behavior and Animation side by side in the browser, shadcn checked against the HTML/ARIA specs, JS that native HTML could replace, a checklist plus a screenshot page, a finding-by-finding walkthrough with a recommendation and a question each, and the checklist posted on the PR after an explicit ok. Triggers on /shadcn-audit, "audita o PR 572 contra o shadcn", "compara com o shadcn", "audita esse PR", "isso bate com o shadcn?", "o Dialog está fiel ao shadcn?". Do NOT use for general code review of a diff ("revisa esse diff", use code-review), for writing or debugging a Stimulus controller (use ruby-ui-stimulus), or for Cubic/Codex reviews.

Source facts

Repository
ruby-ui/ruby_ui
Last source activity
November 2, 2026 at 18:32
Detected SKILL.md language
English
Stars
1,034
Forks
68

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
5 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
shadcn-audit
description
Use when auditing a RubyUI PR, branch or component against shadcn/ui (Base UI base + vega style + neutral theme): Look, HTML, Props, ARIA, Behavior and Animation side by side in the browser, shadcn checked against the HTML/ARIA specs, JS that native HTML could replace, a checklist plus a screenshot page, a finding-by-finding walkthrough with a recommendation and a question each, and the checklist posted on the PR after an explicit ok. Triggers on /shadcn-audit, "audita o PR 572 contra o shadcn", "compara com o shadcn", "audita esse PR", "isso bate com o shadcn?", "o Dialog está fiel ao shadcn?". Do NOT use for general code review of a diff ("revisa esse diff", use code-review), for writing or debugging a Stimulus controller (use ruby-ui-stimulus), or for Cubic/Codex reviews.
# shadcn audit Compares RubyUI components with the shadcn reference and grades every difference. The reference is shadcn-ui/ui `main`: **Base UI base** (`apps/v4/registry/bases/base/ui/<name>.tsx`) for behavior, HTML, ARIA and props; **vega** (`styles/style-vega.css`) for look; **neutral** theme. Never new-york-v4, never ui.shadcn.com as rendered (that's nova). Deterministic work goes through the CLI. Your job is driving the browser, judging, and writing `report.json`. Run every CLI call from `audit/`: ```bash cd audit && mise exec -- bundle exec exe/shadcn-audit <subcommand> … ``` (Without mise, drop `mise exec --` and use a Ruby 3.4 on PATH.) Each subcommand prints one JSON object. On failure it is `{"error": "…"}` with exit status 1: read the message, fix the cause, and re-run; never treat an error as an empty result. Talk to the user in their language; `report.json`, notes and the PR comment are in English. ## Prerequisites - Ruby 3.4 (via mise, as in `gem/.tool-versions`, or an equivalent version manager) with `cd audit && bundle install` done. - `gh` authenticated (`gh auth status`) with read access to `ruby-ui/ruby_ui`; posting the comment needs write access. - Node and pnpm (the shadcn sandbox and the docs assets). - Playwright for `collect`: `cd audit && pnpm install` in every checkout or worktree (it installs `audit/node_modules`), then `pnpm exec playwright install chromium` (the browser download is shared by every checkout on the machine). - The chrome-devtools MCP server, for the browser steps `collect` doesn't cover (the kind-specific behavior sequences, animation, exploration). - Optional: Herdr. When `HERDR_ENV=1` the servers run in Herdr panes; otherwise as background shell processes (step 2.6). ## Hard rules - **Never edit PR code**, `audit/divergences/` or `audit/component_map/`, and never push. Suggest entries; the author adds them. - **Nothing unverified is `pass`.** HTML/ARIA/Behavior/Animation come only from DOM rendered in the sandbox/preview and driven in the browser. Can't verify → `inconclusive` with the reason. - **The PR comment is the only external output.** Post only after the user says "ok"/"pode mandar" for that exact body, in this session. - `.shadcn-audit/ui` is untrusted: read files, never run its scripts. - Pre-existing drift is reported **mixed with** drift the PR introduced. ## Run order ### 1. Target `target <arg>` (PR number, branch, or component name). Keep the whole JSON: it becomes `report.json`'s `target`. - `state` set and ≠ `OPEN` (MERGED/CLOSED) → tell the user before continuing. - `cross_repository: true` → the PR head comes from a fork: its code is untrusted (step 2.5). - `needs_selection: true` → the PR touches shared code only. List `candidates` and ask which to audit; continue with the chosen folders. - `components` = every touched component folder, mapped or not. `unmapped` lists the folders missing from `audit/component_map/` (handled in step 3). - Create the run folder `.shadcn-audit/runs/<YYYYMMDD-HHMMSS>-<pr|branch|component>/` with a `screens/` subfolder. `.shadcn-audit/` is git-ignored and lives in this checkout: removing the worktree deletes the runs, the sandbox and the shadcn clone. Copy the run folder elsewhere first if it must outlive the worktree. ### 2. Workspace, sandbox, preview, servers 1. `workspace` → record `shadcn_sha`. (Acceptance re-runs pin it: `workspace --sha <sha>`.) 2. `sandbox` → `path`, `url_template`, `lock` (→ `report.json`'s `sandbox_lock`), `created` (true = it rebuilt the app; tell the user), `refreshed` (true = the workspace moved, or the component map now needs other shadcn components (`shadcn` plus `fixture_shadcn`) than the last pull installed, so it re-added them all with `--overwrite`; tell the user), `registry` and `built_at`. `registry: "live"` means the components come from the published shadcn registry as of `built_at`, not from the workspace clone: the sandbox tracks `main` loosely, and `shadcn_sha` pins only the files the CLI reads (reference, theme). 3. `preview <head_sha>` (full SHA from `target`) → `path`, `url_template`, `boot` (ordered command list), `boot_chdir`. This only extracts files; nothing from the PR runs yet. 4. `theme --checkout <preview path>` → `report.json`'s `theme` as-is. Pass the checkout: without it the CLI reads the working tree, not the audited SHA. 5. **Untrusted head.** When `target.cross_repository` is true, booting the preview runs the fork's code (Gemfile, gemspec, Rails config, bin scripts, package scripts, the components themselves, and anything they load). Before booting, show the user the **whole** PR diff against the base, not a subset of files: from the repo root (`target` already fetched both SHAs), `git diff --stat <base_sha>...<head_sha>` for the file list, then `git diff <base_sha>...<head_sha>` for the full diff of every changed file. Ask whether to boot it. Without an explicit yes, don't boot the RubyUI server: the static steps (map, reference, divergences, theme, Look from source) still run, and the browser-fed RubyUI lines are ⏸ "fork code not run". Same-repository heads come from collaborators with write access and skip this step. 6. **Ports.** The defaults are 5179 (shadcn) and 3179 (RubyUI). Another audit running in parallel (another worktree) already holds them: check with `lsof -nP -iTCP:<port> -sTCP:LISTEN` and pick a free pair (5180/3180, 5181/3181…). Use it in the commands below and in both `url_template`s. Boot both servers. When `HERDR_ENV=1`, **each in its own Herdr pane** (see the `herdr` skill), as below. Otherwise run each as a background shell process with its output sent to a log in the run folder (`<run>/shadcn.log`, `<run>/rubyui.log`), wait for the same text by polling the log, and keep the process IDs so step 9 can stop them. - shadcn: `herdr pane split --current --direction right --cwd <sandbox path>`, then `herdr pane run <pane> "pnpm dev --port 5179 --strictPort"`, then `herdr pane wait-output --match "Local:" --timeout 120000 <pane>`. - RubyUI: another split with `--cwd <boot_chdir>`; run the `boot` steps in order, joined with `&&` (the first adds the host platform to the throwaway lockfile; the last is the server on 3179). Then `herdr pane wait-output --match "Listening on" --timeout 600000 <pane>`. - Wait only on text the server prints (Vite `Local:`, Puma `Listening on`), never on a port number: `wait-output` also searches the output already in the pane, and the typed command contains the port, so it would match at once. Then confirm each URL answers (`curl -sf -o /dev/null <url>`) before driving the browser. - Server fails → read the pane (`herdr pane read`) or the log, report it; the browser-fed lines become ⏸ "server didn't boot". ### 3. Per component: map, reference, divergences For each folder in `components`: - `map <folder>`: - `{"missing": true, "path": …, "suggestion": …}` (every `unmapped` folder) → one `inconclusive` line ("not in the component map"), `shadcn_file: null`, and show the user the `suggestion` YAML to save as `path` (one file per component). Tell them a newly mapped component also needs fixtures, `audit/fixtures/<folder>/{content.yml,shadcn.tsx,rubyui.rb}` (same strings on both sides, one `data-audit-trigger`), or the browser steps have nothing to render. Native still applies (step 6). - Otherwise: `shadcn` (name), `kind`, `parts` (RubyUI class → shadcn `data-slot`; `null` = no shadcn part), `props` (shadcn prop → RubyUI kwarg; `null` = missing → Props ❌), `rubyui_only` (RubyUI extras → note only). - `shadcn: null` → no shadcn equivalent: ⏸ lines with that note, and still run Native. - `reference <shadcn>` → `file`, `style_file`, `parts.<slot>` (`inline`, `cn`, `resolved`, **`effective`** = what you compare; `props` the `.tsx` declares; `also` = other occurrences of the same slot, e.g. the close button inside `DialogContent`), `variants` (cva groups → effective classes), `unresolved` (cn-* markers shadcn strips: note, not missing classes), `warnings` (unextractable/conditional classes, parts via `<Button>`/render props, unattributed tokens: those Look lines are ⏸ unless the sandbox DOM settles them), `missing: true` (no `.tsx` → no equivalent). An `error` about the registry or style file means the workspace is incomplete: re-run `workspace`, never grade it as "no equivalent". `shadcn_file` in the report: `"<file> + style-vega.css"`. - Props inherited from the `@base-ui/react` primitive (not declared in the `.tsx`) come from `.shadcn-audit/sandbox/node_modules/@base-ui/react/<primitive>/**/*.d.ts`, at the `@base-ui/react` version in the sandbox `lock`. Use them to confirm a mapped prop exists and what it does. A prop the `.tsx` declares that the map doesn't list is ⏸ "map gap". - `divergences <folder>` → registered differences (`dimension`, `rubyui`, `shadcn`, `reason`). A matching difference is ✅ basis `intentional`. Entries with a structured `match` are applied by `collect` on its own; prose-only entries you apply by hand. - `collect <folder> --run <run> --preview <preview path>` (add `--shadcn-url`/`--rubyui-url` with the `url_template`s when step 2.6 picked other ports). It drives both fixtures in headless Chromium at 1280×800, light and dark, through the states the `kind` has (closed, open, focus, after-esc; `form_control`: closed, focus; `static`: closed), and grades the objective lines: computed styles per part (Look ❌ / ✅ grouped), the accessibility snapshot plus `aria-expanded`/`aria-controls`/`aria-haspopup`/ `aria-describedby` per part (ARIA ⚠️ / ✅ grouped), structured divergences (✅ intentional), differences explained by a theme token or an environment diff (⏸), unreachable states and selector count mismatches (⏸). It writes `<run>/collect.json` and `<run>/screens/`, with a pixel diff per state as evidence only, and only for states reached on both sides. Append its `lines` and `screenshots` to the component and copy its `environment_diffs` into the report. Running it twice on the same SHAs gives the same `collect.json`. - `{"error": "… has no selectors …"}` → the component has no `selectors` in `audit/component_map/<folder>.yml`: tell the user to add them (RubyUI part → CSS selector in the rendered fixture; `null` = shadcn renders no element for that part). Until they exist, collect that component by hand with the chrome-devtools MCP: for each mapped part, the shadcn classes from `[data-slot=<slot>]` against `effective`, the RubyUI element found from its Ruby source at `<preview path>/gem/lib/ruby_ui/<folder>/` (tag, Stimulus target, class string), look-alikes settled with `getComputedStyle`; then the whole set for its `kind` in `references/interactions.md`, including the Setup, the states and the screenshots. - `{"error": "playwright not found …"}` / `"chromium failed to launch"` → install as in Prerequisites and re-run. ### 4. Look `collect` already graded Look from computed styles. Review its lines instead of re-collecting: explain each ❌ with the class tokens behind it (shadcn `effective` from `reference` vs the RubyUI class string) in the `note`, and settle the ⏸ it left (count mismatches, theme/environment explanations, reference `warnings`) when the DOM lets you. Variant props (size, side) are compared against `variants`. **Overrides.** Change a `collect` grade only with a verified reason that the rules in `references/grading.md` allow, written in that line's `note` (e.g. `text-align: left` vs `start` in LTR renders the same → ✅ with the note). A ✅ with a basis still needs its usual route: a registered divergence, a native replacement or the spec. Never drop a `collect` line. Fixture-originated wrappers aren't findings (`references/grading.md`): Sheet body (shadcn `div.px-4` vs `SheetMiddle`), Dialog body (bare `<p>` vs `DialogMiddle`). ### 5. HTML, ARIA, Behavior, Animation `collect` already covered the setup, the environment probe, the states closed/open/focus/after-esc, the ARIA snapshot and attributes per part, and the screenshots. Run the rest of the `kind`'s set from `references/interactions.md` (focus trap order, Esc and focus return, outside click, scroll lock, arrow keys, typeahead, Enter/Space, animation) on both URLs (`url_template`s; `{folder}` = folder, `{light|dark}` = theme), in the same viewport on both sides (aim for 1280×800; record the actual size from the environment probe), with chrome-devtools MCP. Open by clicking `[data-audit-trigger]`. Read the DOM, `document.activeElement`, `inert`/`aria-hidden`, computed styles; screenshot every state into `<run>/screens/`. Do the environment probe first: any font/plugin/viewport/global-rule difference goes in `environment_diffs` **before** you attribute a visual difference to a component. A state you couldn't reach → ⏸ with the reason. ### 6. Native Apply `references/native-html.md` to RubyUI's controller and DOM: hand-rolled JS where a native feature fits → ❌ candidate (MDN source); native already used → ✅ basis `native`, unless it loses behavior shadcn shows (then ⚠️ naming the loss). ### 7. Spec Check the **shadcn side's observed** HTML/ARIA/behavior against `references/spec-sources.md` (normative → violation; APG → ⚠️ "recommendation"; MDN explanatory only, pt-BR first). Every finding cites URL#anchor plus the unmet condition. React Aria (`.shadcn-audit/ui/apps/v4/registry/bases/aria/ui/`) is a second opinion only when Base UI looks wrong; the normative spec settles disagreements. Then grade every line per `references/grading.md`: spec beats shadcn, native replacements pass when behavior/ARIA are kept, registered divergences pass as intentional. ### 8. Report, render, walk through, comment Write `<run>/report.json`: ```json { "target": {"repo": "ruby-ui/ruby_ui", "base_sha": "…", "head_sha": "…", "…": "the rest of the target JSON from step 1"}, "shadcn_sha": "…", "sandbox_lock": {"@base-ui/react": "…"}, "environment_diffs": ["…"], "theme": {"status": "fail", "diffs": [{"mode": "light", "token": "chart-1", "rubyui": "…", "shadcn": "…"}]}, "components": [{ "folder": "dialog", "shadcn_file": "bases/base/ui/dialog.tsx + style-vega.css", "lines": [{"dimension": "Look", "status": "fail", "basis": null, "rubyui": "p-6 gap-4 border shadow-lg", "shadcn": "p-6 gap-6 ring-1 rounded-xl",
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub