diff --git a/.claude/skills/shadcn-audit/SKILL.md b/.claude/skills/shadcn-audit/SKILL.md index b30f30f63..7a26d220f 100644 --- a/.claude/skills/shadcn-audit/SKILL.md +++ b/.claude/skills/shadcn-audit/SKILL.md @@ -54,7 +54,7 @@ an error as an empty result. Talk to the user in their language; ## Hard rules -- **Never edit PR code**, `audit/divergences.yml` or `audit/component_map/`, +- **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 → diff --git a/.claude/skills/shadcn-audit/references/grading.md b/.claude/skills/shadcn-audit/references/grading.md index bd111e65f..54b1e648c 100644 --- a/.claude/skills/shadcn-audit/references/grading.md +++ b/.claude/skills/shadcn-audit/references/grading.md @@ -21,7 +21,7 @@ Behavior or Animation. When unsure, use ⏸ and say what's missing. | `basis` | Shows as | Rule | | --- | --- | --- | -| `intentional` | ✅ … — intentional | The difference matches an entry from `divergences ` (same `dimension`). `source` = `audit/divergences.yml`, `note` = its `reason`. Entries with a structured `match` are applied by `collect` automatically; prose-only entries by hand. | +| `intentional` | ✅ … — intentional | The difference matches an entry from `divergences ` (same `dimension`). `source` = `audit/divergences/.yml`, `note` = its `reason`. Entries with a structured `match` are applied by `collect` automatically; prose-only entries by hand. | | `native` | ✅ … — native | RubyUI swaps shadcn's JS for a native feature **and keeps** what shadcn delivers: focus in and back, Esc, dismiss, inertness of the page behind. Labelling is markup, graded on the ARIA line, not here. Tag differences go in `note`. No registration needed. | | `spec` | ✅ … — follows spec | shadcn breaks a spec (stage 2) and RubyUI follows it. `source` = the spec URL#anchor. No registration needed. | @@ -104,7 +104,7 @@ before blaming the component. - Pre-existing drift is reported **mixed with** drift the PR introduced. No "old debt" section, no filtering by changed lines. -- The audit never edits PR code, `divergences.yml` or `audit/component_map/`. +- The audit never edits PR code, `audit/divergences/` or `audit/component_map/`. When a difference looks deliberate, show a ready-to-paste divergences entry (fields: `component`, `dimension`, `rubyui`, `shadcn`, `reason`, `since`) for the PR author to add by hand. `component`, `rubyui`, `shadcn` and @@ -120,7 +120,7 @@ From `audit/test/fixtures/report_572.json` (pins: head `a9a5d1fa`, base | Dialog ✅ Look `overlay bg-black/10 + supports-backdrop-filter:backdrop-blur-xs` | Effective vega classes for `dialog-overlay` match the RubyUI overlay's classes in the DOM. `source: style-vega.css:464`. | | Dialog ❌ Look `p-6 gap-4 border shadow-lg sm:rounded-lg max-w-lg` vs `p-6 gap-6 ring-1 ring-foreground/10 rounded-xl sm:max-w-md` | Look difference, not registered → ❌. Both values concrete, `source: style-vega.css:468`. | | Dialog ⚠️ Animation "no exit fade" vs `data-closed:fade-out-0 duration-100` | Seen in the browser: shadcn's popup runs an animation on close, RubyUI's disappears at once. Animation → ⚠️, `source: style-vega.css:468`. | -| Dialog ✅ HTML native `` — intentional | `divergences dialog` has the HTML entry → basis `intentional`, `source: audit/divergences.yml`. | +| Dialog ✅ HTML native `` — intentional | `divergences dialog` has the HTML entry → basis `intentional`, `source: audit/divergences/dialog.yml`. | | Dialog ⚠️ ARIA "no aria-labelledby/aria-describedby" | Base UI wires Title/Description in the sandbox DOM; RubyUI doesn't. ARIA → ⚠️, `source: bases/base/ui/dialog.tsx:122-138`. (A dialog with no accessible name also breaks WAI-ARIA 1.2 `#dialog` "Accessible Name Required", which is a separate ❌ Spec line, rule 5.) | | Dialog ⏸ Behavior "scroll lock not exercised" | The state wasn't reached. Never ✅ by default. | | Dialog ✅ Native `showModal()` — native | Native `` gives inert, Esc and focus. Scroll lock is kept on both sides (RubyUI's controller adds `overflow-hidden` to `body`; Base UI locks it too), so nothing shadcn delivers is lost. `source` = MDN showModal. | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b13753e2..82a520b9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,8 +124,9 @@ jobs: run: bundle exec exe/ruby-ui-mcp-build - name: Fail on diff run: | - if ! git diff --exit-code mcp/data/registry.json; then - echo "registry.json out of date — run 'cd mcp && bundle exec exe/ruby-ui-mcp-build' and commit" + git add -N mcp/data/registry + if ! git diff --exit-code mcp/data/registry; then + echo "mcp/data/registry/ out of date — run 'cd mcp && bundle exec exe/ruby-ui-mcp-build' and commit" exit 1 fi diff --git a/CLAUDE.md b/CLAUDE.md index 831b8cea8..f641fbb5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,7 +11,7 @@ Independent subprojects (gem, docs, mcp, audit), each with its own `Gemfile` and | [`gem/`](gem/) | The `ruby_ui` gem — Phlex components, generators, gemspec, Minitest suite. Published to RubyGems from here. | | [`docs/`](docs/) | Rails 8 app powering https://rubyui.com. Consumes the local gem via `path: "../gem"`. | | [`audit/`](audit/) | shadcn fidelity audit tooling; skill in [`.claude/skills/shadcn-audit/`](.claude/skills/shadcn-audit/). | -| [`mcp/`](mcp/) | MCP server for ruby_ui; `mcp/data/registry.json` is generated from the gem's component source and parsed docs. | +| [`mcp/`](mcp/) | MCP server for ruby_ui; `mcp/data/registry/` (one JSON per component plus `index.json`) is generated from the gem's component source and parsed docs. | | `.github/workflows/ci.yml` | Unified CI: `gem` tests (Ruby 3.3 + 3.4), `docs` Rails tests, `mcp` tests + registry check, `audit` (shadcn-audit CLI) tests, docker build for the docs devcontainer. | Subproject-specific instructions live in `gem/AGENTS.md` and `docs/CLAUDE.md` (→ `docs/AGENTS.md`). Read those before editing inside a subdir. @@ -21,8 +21,8 @@ Subproject-specific instructions live in `gem/AGENTS.md` and `docs/CLAUDE.md` ( - Component code → `gem/lib/ruby_ui//`. Tests → `gem/test/ruby_ui/_test.rb`. - Component docs view → `docs/app/views/docs/.rb`. Update in same PR as the component. - Generator/installer logic → `gem/lib/generators/ruby_ui/`. Dependency map → `gem/lib/generators/ruby_ui/dependencies.yml`. -- Any change under `gem/lib/ruby_ui/`, to `gem/lib/generators/ruby_ui/dependencies.yml`, or to `VERSION` in `gem/lib/ruby_ui.rb` → also rebuild the MCP registry (`cd mcp && bundle exec exe/ruby-ui-mcp-build`) and commit `mcp/data/registry.json` in the same PR. CI's "MCP registry up to date" check fails otherwise. -- Shadcn fidelity audit tooling and deliberate divergences → `audit/` (`audit/divergences.yml`). +- Any change under `gem/lib/ruby_ui/`, to `gem/lib/generators/ruby_ui/dependencies.yml`, or to `VERSION` in `gem/lib/ruby_ui.rb` → also rebuild the MCP registry (`cd mcp && bundle exec exe/ruby-ui-mcp-build`) and commit `mcp/data/registry/` in the same PR. CI's "MCP registry up to date" check fails otherwise. +- Shadcn fidelity audit tooling and deliberate divergences → `audit/` (`audit/divergences/.yml`, one file per component). - Site chrome, routes, marketing pages → `docs/app/`. - A component's Stimulus controller lives only in `gem/lib/ruby_ui//_controller.js`. `docs/app/javascript/controllers/ruby_ui/_controller.js` is a symlink to it, not a copy — editing the gem file is enough for existing components. A brand-new component's controller needs `docs`' `rake ruby_ui:sync_controller_symlinks` to create the symlink, plus `bin/rails stimulus:manifest:update` to register it. @@ -67,7 +67,7 @@ Decide the bump with SemVer against commits since the last `vX.Y.Z` tag: new bac 3. **Reflect the version on the docs website (PR against `main`).** Releases go through a `[Release] vX.Y.Z` PR (main is protected — no direct pushes). The PR bundles, on a `release/vX.Y.Z` branch: - the `gem/lib/ruby_ui.rb` bump + both regenerated `Gemfile.lock`s; - `docs/app/views/pages/home.rb` — update the home hero badge copy to the headline features (the header version badge reads `RubyUI::VERSION` and updates automatically; do not hardcode it); - - `mcp/data/registry.json` — rebuild with `cd mcp && bundle exec rake mcp:build` (reads `../gem`, so it picks up the new version). + - `mcp/data/registry/` — rebuild with `cd mcp && bundle exec rake mcp:build` (reads `../gem`, so it picks up the new version). 4. **Tag + GitHub release.** After the release PR merges, tag `vX.Y.Z` on the merge commit, push the tag, and cut the GitHub release. The git tag and the published gem must both exist — publishing the gem without tagging (or vice versa) is an incomplete release. @@ -100,6 +100,6 @@ Decide the bump with SemVer against commits since the last `vX.Y.Z` tag: new bac - Don't edit a component without updating its `docs/app/views/docs/.rb` counterpart. - Don't bump the gem version from `docs/`; `RubyUI::VERSION` lives in `gem/lib/ruby_ui.rb` and the gem is published from `gem/`. (The release PR also reflects the version into `docs/` and `mcp/` — see [Releasing](#releasing).) - Don't call a release done after only publishing the gem — tag `vX.Y.Z` and reflect the version on the website too. -- Don't push a change under `gem/lib/ruby_ui/`, to `dependencies.yml` or to `VERSION` without the rebuilt `mcp/data/registry.json`. +- Don't push a change under `gem/lib/ruby_ui/`, to `dependencies.yml` or to `VERSION` without the rebuilt `mcp/data/registry/`. - Don't use ui.shadcn.com or `new-york-v4` as the reference. - Don't commit secrets; each app uses local env config. diff --git a/audit/README.md b/audit/README.md index 16b82b9db..041091663 100644 --- a/audit/README.md +++ b/audit/README.md @@ -10,7 +10,7 @@ mise exec -- bundle exec exe/shadcn-audit workspace [--sha SHA] `workspace` keeps a sparse clone of `shadcn-ui/ui` in `.shadcn-audit/ui` (git-ignored) and checks out `main`, or the given SHA. -`collect --run [--preview ] [--shadcn-url T] [--rubyui-url T]` drives both fixtures in headless Chromium (playwright-ruby-client) with the sandbox and preview servers running, and writes `/collect.json`: computed styles, accessibility snapshots, screenshots and pixel diffs per state, plus the objectively graded Look/ARIA lines. It needs `pnpm install && pnpm exec playwright install chromium` here once. Part selectors come from `selectors` in `component_map/.yml`; a `match` key in `divergences.yml` lets `collect` apply that divergence on its own. +`collect --run [--preview ] [--shadcn-url T] [--rubyui-url T]` drives both fixtures in headless Chromium (playwright-ruby-client) with the sandbox and preview servers running, and writes `/collect.json`: computed styles, accessibility snapshots, screenshots and pixel diffs per state, plus the objectively graded Look/ARIA lines. It needs `pnpm install && pnpm exec playwright install chromium` here once. Part selectors come from `selectors` in `component_map/.yml`; a `match` key in `divergences/.yml` lets `collect` apply that divergence on its own. Tests use `ShadcnAudit::FakeShell` (`test/test_helper.rb`): stub canned stdout by argv or matcher, then assert on recorded calls. diff --git a/audit/component_map/badge.yml b/audit/component_map/badge.yml index af1b98553..745aad7b6 100644 --- a/audit/component_map/badge.yml +++ b/audit/component_map/badge.yml @@ -6,7 +6,7 @@ parts: selectors: Badge: '[data-slot="badge"]' props: - # render: RubyUI covers its one badge use, , with href: (see divergences.yml). + # render: RubyUI covers its one badge use, , with href: (see divergences/badge.yml). Badge: {variant: variant, render: href} rubyui_only: Badge: [size] diff --git a/audit/component_map/breadcrumb.yml b/audit/component_map/breadcrumb.yml index 476ff28f7..8097ffe35 100644 --- a/audit/component_map/breadcrumb.yml +++ b/audit/component_map/breadcrumb.yml @@ -18,5 +18,5 @@ selectors: BreadcrumbSeparator: '[data-slot="breadcrumb-separator"]' BreadcrumbEllipsis: '[data-slot="breadcrumb-ellipsis"]' props: - # render: RubyUI covers the link case with href: (see divergences.yml). + # render: RubyUI covers the link case with href: (see divergences/breadcrumb.yml). BreadcrumbLink: {render: href} diff --git a/audit/component_map/button.yml b/audit/component_map/button.yml index 92be2a0fd..6f6cebcf3 100644 --- a/audit/component_map/button.yml +++ b/audit/component_map/button.yml @@ -9,7 +9,7 @@ props: # shadcn default variant = RubyUI :primary; size default = RubyUI :md; # size icon/icon-xs/icon-sm/icon-lg = RubyUI icon: true with size :md/:xs/:sm/:lg # (size: :icon, :icon_sm… work too). render: RubyUI covers its one button use, - # , with href: (see divergences.yml). + # , with href: (see divergences/button.yml). Button: {variant: variant, size: size, render: href, focusableWhenDisabled: focusable_when_disabled} # icon is the counterpart of shadcn's icon* sizes (see props); size :xl is a # RubyUI extra value of size. diff --git a/audit/component_map/carousel.yml b/audit/component_map/carousel.yml index 549e0ae49..b1528292f 100644 --- a/audit/component_map/carousel.yml +++ b/audit/component_map/carousel.yml @@ -14,7 +14,7 @@ selectors: CarouselPrevious: '[data-slot="carousel-previous"]' CarouselNext: '[data-slot="carousel-next"]' props: - # plugins/setApi: see divergences.yml. + # plugins/setApi: see divergences/carousel.yml. Carousel: {opts: options, plugins: null, orientation: orientation, setApi: null} # Previous/Next forward attrs to Button, so variant:/size: reach it. CarouselPrevious: {variant: variant, size: size} diff --git a/audit/divergences.yml b/audit/divergences.yml deleted file mode 100644 index a88f65708..000000000 --- a/audit/divergences.yml +++ /dev/null @@ -1,137 +0,0 @@ -# Intentional RubyUI-vs-shadcn differences. A divergence listed here makes the -# matching finding a `pass` with basis `intentional`. -# Optional `match: {part, property (Look) | attribute (ARIA, or `snapshot`), rubyui, shadcn}` lets `collect` apply it -# automatically; entries without it are applied by the agent. -- component: dialog - dimension: HTML - rubyui: " + showModal()" - shadcn: "
" - reason: "Native modal dialog: the browser provides inert, Esc and focus handling." - since: "2026-10-26" -- component: dialog - dimension: HTML - rubyui: "DialogTrigger
wrapping its child; ARIA set on the first interactive descendant" - shadcn: "trigger is the