From a3b34a8e6243ff3bef5bdae02fc0b5950b573524 Mon Sep 17 00:00:00 2001 From: Cirdes Date: Sun, 20 Sep 2026 20:57:05 -0300 Subject: [PATCH 01/13] [Documentation] Plan for Phase 2.1: the hard components first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dialog, Toggle and ToggleGroup, Select, Data Table and ThemeToggle from Phlex to the 2.0 layer, with docs/ pinned to the published 1.6.0 first. Written from spec §6.2.1, decisions 1–10 and plan 2.0b's exclusions; every shape it relies on was measured through the harness before it was written (its "What this plan measured" and Appendix A). It takes four decisions of its own — mixed_attrs, the whitespace-tight sidecar layout with the layer dropping the file's final newline, inherited sidecars, and ThemeToggle joining 2.1 — and writes out the four composite sidecars so they can be judged before execution. Co-Authored-By: Claude Fable 5.1 --- ...hase-2-1-hard-components-implementation.md | 3636 +++++++++++++++++ 1 file changed, 3636 insertions(+) create mode 100644 design/plans/2026-09-20-phase-2-1-hard-components-implementation.md diff --git a/design/plans/2026-09-20-phase-2-1-hard-components-implementation.md b/design/plans/2026-09-20-phase-2-1-hard-components-implementation.md new file mode 100644 index 00000000..5a54867a --- /dev/null +++ b/design/plans/2026-09-20-phase-2-1-hard-components-implementation.md @@ -0,0 +1,3636 @@ +# Phase 2.1 — The Hard Components Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the first components from Phlex to the 2.0 layer — Dialog (8 classes), Toggle and ToggleGroup (3), Select (8), Data Table (16, plus 3 adapters that emit no HTML) and ThemeToggle (1, pulled in because it renders Toggle) — with their 22 golden scenarios byte-identical to the frozen 1.6 snapshots in both the canonical and the strict form, their Stimulus controllers unedited, their unit tests ported and none deleted, and `docs/` pinned to the published 1.6.0 gem first so the site keeps building while the gem is mid-migration. + +**Architecture:** Each component becomes a plain Ruby class inheriting `RubyUI::Component` (the layer Phase 2.0a built) beside a `.html.erb` sidecar that renders the computed attributes with `tag.attributes(component.attrs)` and the captured block with `component.content`. The ERB fixtures Phase 2.0b wrote are the ruler: they render the same composition before and after the migration, against the same snapshots, so a migration changes only an implementation. A migrated component's scenarios lose their Phlex block (the `Phlex::Kit` method that block calls no longer exists), leaving the ERB fixture as the only lane. Three gaps in the layer that these components expose are closed first: the nested attribute hash a component forwards to a neighbour (`mixed_attrs`), the sidecar file's final newline (dropped by `render_in`), and a subclass without a sidecar (renders its nearest ancestor's). + +**Tech Stack:** Ruby 3.3 and 3.4 in CI (4.0.2 locally; 3.4.7 through `mise` for `docs/`), Minitest, ActionView / Railties 8.1.3.1, ReActionView 0.4.1, Herb 0.10.4, Phlex 2.4.1 and phlex-rails 2.4.0 (development only, for the components that have not migrated yet), tailwind_merge 1.5.5, Nokogiri. + +**Spec:** `design/2026-09-19-rubyui-2-0-design.md` — §4.1 (anatomy), §4.3 (the layer's differences from 1.6), §6 "Phase 2.1 The hard components first" and §6 "Phase 2.2" (the per-component definition of done), §9.2 (upstream questions). Decisions 1–10 in `design/v2/decisions.md`, especially 7 (every fixture exists before any migration), 8 (the strict lane), 9 (scoped sidecar lookup) and 10 (what Herb's trim mode does; the sidecar layout was left to this plan). The "Not in this plan" section of `design/plans/2026-09-20-phase-2-0b-fixtures-implementation.md` is where this plan was scoped, and its "Translation rules" and Appendix A are the rules every fixture already follows. `design/v2/follow-up-issues.md` item 1 (#537, DataTable's nested forms) is ported as it is, not fixed here. + +## Global Constraints + +- Branch `v2/hard-components`, created from `v2/fixtures` (PR #554, unmerged, tip `8ac23ac`); the PR for this plan targets `v2/fixtures`. Never branch from, rebase onto, or push to `main`. Never push before Task 9 asks. +- Work in `gem/` for every task but Task 1 (`docs/`) and the registry rebuilds (`mcp/`). Run every command from the directory the step names. `ENV["RAILS_ENV"]` is `test` for every gem test run (the helper sets it). +- **No snapshot changes.** Never run `bundle exec rake golden:update` in this plan. `git status --porcelain gem/test/golden/snapshots gem/test/golden/strict` (from the repo root) is empty at the end of every task. Never hand-edit a file under either directory. A migration that needs a snapshot change is a port defect or a STOP (see "When a golden test fails"), never a re-record. +- **No fixture changes.** `gem/test/golden/views/**` is not touched: the fixtures are the translation of the scenarios and were proved against the snapshots with the components still Phlex (plan 2.0b). If a fixture seems wrong, that is a STOP. +- **The Stimulus controllers are unedited.** `git diff --quiet v2/fixtures -- 'gem/lib/ruby_ui/**/*.js'` succeeds at the end of every task. +- **The ruler does not change.** `gem/test/golden/harness.rb`, `catalog.rb`, `canonical_html.rb` and `golden_test.rb` are not touched. `gem/test/golden/scenarios.rb` changes only by removing the Phlex blocks of migrated scenarios (Tasks 3–7) and one paragraph of its header comment (Task 8). +- **A sidecar emits no whitespace of its own** (decision 12, taken in Task 2): no whitespace between `>` and `<`, none at a text–element boundary, a line break only inside an ERB tag or inside an HTML start tag between two attributes, and the file ends with exactly one newline (which `render_in` drops). The sidecar rules below are the complete list; every sidecar in this plan is written out in full — copy it, do not reformat it. +- **`attrs` is flat; `mixed_attrs` is forwarded.** A sidecar hands `component.attrs` (String keys, serialized) to `tag.attributes`. Ruby that builds another component from a component's attributes, or merges more attributes in before serializing, uses `mixed_attrs` (Symbol keys, nested, classes merged — 1.6's `attrs`). Never splat `attrs` into a `.new`. +- **A migrated class has no Phlex in it:** no `view_template`, no `< Base`, no Phlex element call, no `plain`, no `safe` except the one `Phlex::SGML::SafeValue` `DataTablePerPageSelect` needs for a neighbour that is still Phlex (Task 7, with its comment). +- **Every `bundle exec rake golden` and `bundle exec rake test` in this plan ends with `0 failures, 0 errors, 0 skips`**, at the counts the progress table gives. StandardRB is green at every commit. `mcp/data/registry.json` is rebuilt (`cd mcp && bundle exec exe/ruby-ui-mcp-build`) and committed in every task that changes a file under `gem/lib/ruby_ui/`. +- Every commit message ends with `Co-Authored-By: Claude Fable 5.1 `. + +--- + +## What this plan measured — 2026-09-20 + +Every row was rendered through the real path — a template under the gem's `Rails.root`, compiled by ReActionView 0.4.1's handler and so by Herb 0.10.4 with `validation_mode = :raise` — on `v2/fixtures` at `8ac23ac`, Ruby 4.0.2. Probe sources are in Appendix A. "Strict-identical" means `Golden::CanonicalHtml.call(html, strict: true)` equal to the same form of the raw Phlex output. + +| Question | Probe | Result | +| --- | --- | --- | +| A 2.0 component forwards its attributes to a Phlex neighbour (`Checkbox.new(**…)`): the nested hash, or the flat one? | 1a, 1b | The nested hash (`Attributes.mix(default_attrs, user_attrs)`) is strict-identical to 1.6's `DataTableRowCheckbox`. The flat hash is not: `"data-action"` lands beside Checkbox's `data: {action:}` and **two `data-action` attributes** reach the page where 1.6 concatenated them. Decision 11. | +| `yield(self)` with a component-scoped method — `do |group| … group.ToggleGroupItem(…) { "L" }` — through `helpers.render(X.new(…), &block)` | 2 | Renders; the `{ "L" }` block's String is the content (ActionView's `capture` returns the block's value when the buffer is empty). Works for a Phlex neighbour (Button) and a 2.0 one (a probe) alike. | +| An inline event handler for a Phlex neighbour: `NativeSelect.new(onchange: Phlex::SGML::SafeValue.new("…"))` from a sidecar, with the `