Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/shadcn-audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 →
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/shadcn-audit/references/grading.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <folder>` (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 <folder>` (same `dimension`). `source` = `audit/divergences/<folder>.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. |

Expand Down Expand Up @@ -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
Expand All @@ -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 `<dialog>` — intentional | `divergences dialog` has the HTML entry → basis `intentional`, `source: audit/divergences.yml`. |
| Dialog ✅ HTML native `<dialog>` — 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 `<dialog>` 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. |
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -21,8 +21,8 @@ Subproject-specific instructions live in `gem/AGENTS.md` and `docs/CLAUDE.md` (
- Component code → `gem/lib/ruby_ui/<component>/`. Tests → `gem/test/ruby_ui/<component>_test.rb`.
- Component docs view → `docs/app/views/docs/<component>.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/<component>.yml`, one file per component).
- Site chrome, routes, marketing pages → `docs/app/`.
- A component's Stimulus controller lives only in `gem/lib/ruby_ui/<component>/<component>_controller.js`. `docs/app/javascript/controllers/ruby_ui/<component>_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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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/<component>.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.
2 changes: 1 addition & 1 deletion audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <folder> --run <dir> [--preview <path>] [--shadcn-url T] [--rubyui-url T]` drives both fixtures in headless Chromium (playwright-ruby-client) with the sandbox and preview servers running, and writes `<dir>/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/<folder>.yml`; a `match` key in `divergences.yml` lets `collect` apply that divergence on its own.
`collect <folder> --run <dir> [--preview <path>] [--shadcn-url T] [--rubyui-url T]` drives both fixtures in headless Chromium (playwright-ruby-client) with the sandbox and preview servers running, and writes `<dir>/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/<folder>.yml`; a `match` key in `divergences/<folder>.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.

Expand Down
2 changes: 1 addition & 1 deletion audit/component_map/badge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ parts:
selectors:
Badge: '[data-slot="badge"]'
props:
# render: RubyUI covers its one badge use, <a>, with href: (see divergences.yml).
# render: RubyUI covers its one badge use, <a>, with href: (see divergences/badge.yml).
Badge: {variant: variant, render: href}
rubyui_only:
Badge: [size]
2 changes: 1 addition & 1 deletion audit/component_map/breadcrumb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
2 changes: 1 addition & 1 deletion audit/component_map/button.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
# <a>, with href: (see divergences.yml).
# <a>, 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.
Expand Down
2 changes: 1 addition & 1 deletion audit/component_map/carousel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading
Loading