Skip to content
Open
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
26 changes: 17 additions & 9 deletions design/2026-09-19-rubyui-2-0-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,10 @@ code.
under `gem/test/golden/views/`, rendered through the harness and compared
against the same frozen snapshot as its own test. With `phlex-rails` loaded a
fixture renders a component that is still Phlex, so all 188 fixtures are
written before any migration — Button's 15 in plan 2.0a, the rest in 2.0b
(decision 7).
written before any migration — Button's 15 in plan 2.0a, the other 173 in
plan 2.0b (decision 7). Every fixture is one line: under Herb 0.10.4, trim
mode keeps the newline after an `end` that follows content on its line and
emits the indentation before an output tag (decision 10).
- Implement the `enum` coercion helper in `Base`.
- Port Phlex's attribute guards into `Attributes` (§4.3), with unit tests
that assert an unsafe name raises and a `javascript:` reference is dropped.
Expand Down Expand Up @@ -357,8 +359,9 @@ a sidecar, or the difference is listed in §4.3.
The remaining ~50 components, in batches.

**Definition of done, per component.** A plain Ruby class with no Phlex; a
sidecar in trim mode; the snapshot matching; the strict lane matching if the
component carries text; a scenario passing the String form of every enum
sidecar that emits no whitespace Phlex did not (decision 10); the canonical
and the strict snapshot matching for every one of its scenarios (decision
8); a scenario passing the String form of every enum
attribute; **its existing unit tests in `gem/test/ruby_ui/` ported to the new
harness, none deleted** — they are the inventory of what the component promises
beyond its markup; the Stimulus controller untouched; the MCP registry rebuilt.
Expand Down Expand Up @@ -488,7 +491,7 @@ all 68 pages rendering; no second copy of a doc page anywhere in `docs/app`.
| --- | --- |
| Does a component render the same HTML as 1.6? | The golden suite's ERB lane, against the frozen snapshot |
| Is an element that was empty still empty, not whitespace-only? | The canonical form, after Phase 1 hardens it (§9.1) |
| Did whitespace at a text boundary change? | The strict lane, raw output, for text-bearing components (Phase 2.0) |
| Did whitespace at a text boundary change? | The strict lane — the canonical form in preserve mode — for every scenario (Phase 2.0, decision 8) |
| Does an attribute reach the element correctly? | The differential test against Phlex 2.4.1 in `Attributes` |
| Is an unsafe attribute still refused? | Unit tests on `Attributes`' guards, not the golden suite |
| Is a sidecar valid HTML? | Herb's validators, over every sidecar, on every CI run |
Expand Down Expand Up @@ -632,8 +635,12 @@ count never named.
all 188 scenarios: **zero** elements with whitespace-only content and
**zero** text–element boundaries carrying a space, so the hardening changes
no recorded snapshot. It changes what Phase 2 is allowed to emit.
2. **Phase 2 sidecars use ERB trim mode**, so they emit no whitespace Phlex
did not.
2. **Phase 2 sidecars emit no whitespace Phlex did not.** Trim mode was the
intended means; measured through Herb 0.10.4 it keeps the newline after
an `end` that follows content on its line and emits the indentation
before an output tag, so an indented one-render-per-line sidecar is not
whitespace-tight. What is: one line, a break only after opening tags with
every `end` adjacent, or a break inside an ERB tag (decision 10).
3. **Phase 2.0 defines a strict lane** — raw output, attribute order
normalized, nothing else — for components that carry text.

Expand Down Expand Up @@ -680,8 +687,9 @@ Review showed the gap is wider than an inline-pair count: `FormField` flips
behaviour on an empty-versus-whitespace-only element (§9.1), which no
inline-adjacency measure names. What bounds it now is §9.1's three-part
resolution — the hardened canonical form catches the empty/whitespace-only
case for every component, trim mode stops the ERB lane emitting what Phlex did
not, and the strict lane compares every scenario in preserve mode (decision 8).
case for every component, the whitespace-tight sidecar rule (decision 10)
stops the ERB lane emitting what Phlex did not, and the strict lane compares
every scenario in preserve mode (decision 8).
Accepted knowingly, with that shape: what remains unseen is what no HTML
comparison can see — layout, focus, the controllers' behaviour — and Phase 3
is where a browser first looks.
Expand Down
1,788 changes: 1,788 additions & 0 deletions design/plans/2026-09-20-phase-2-0b-fixtures-implementation.md

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions design/v2/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,57 @@ it. ReActionView allows `actionview >= 7.0`; the 2.4 gemspec floors Rails at

**Cost if wrong:** installation is first exercised end to end in 2.4 rather
than now.

## 10. Every ERB fixture is one line; what Herb's trim mode removes and what it keeps — 2026-09-20

Decision 8 said fixtures and sidecars are written "one line, or `<%-`/`-%>`".
Measured through the harness — a template under `Rails.root`, compiled by
ReActionView 0.4.1's handler and so by Herb 0.10.4, the engine every fixture
and every 2.0 sidecar compiles with (a `<div><span></div>` probe raised
`ActionView::SyntaxErrorInTemplate`, which Erubi would not) — trim mode does
the following. Each line is reproduced from a probe whose source and output
are in plan 2.0b's Appendix A.

- `-%>` on an output tag (`<%= … -%>`, `<%= … do -%>`) removes the newline
after it. A statement tag (`<% … %>`) alone on its line loses the
indentation before it and the newline after it whether or not it carries
markers (` <% if true %>\nA<% end %>\n` → `A\n`, exactly as with
`<%- … -%>`); mid-line, its `-%>` is ignored — `apply_trim` consults only
`left_trim?` and `at_line_start?`.
- The `end` that closes a `<%= … do %>` block ignores its own `-%>` and `<%-`
for the newline after it (`Body<% end -%>\nAfter` → `Body</div>\nAfter`):
`visit_erb_block_end_node` in `herb/engine/compiler.rb` never reads the
end tag's `-%>`, and its `<%-` changes nothing the line-start rule does not
already do. It trims Erubi-style — the indentation before it
and the newline after it — only when it stands alone at the start of its
line, and then the newline *before* it, the one ending the content line,
stays (`Body\n<% end %>\nAfter` → `Body\n</div>After`).
- Indentation before an output tag is text and is emitted
(`\n <%= render … -%>` puts two spaces in the output).

So an indented, one-render-per-line layout is not whitespace-tight under this
combination: every content line ending in `end` puts a newline into the
output, every indented `<%=` puts its indentation there, and the strict lane
sees both. Two layouts do emit nothing: a break only after each opening tag,
with every `end` kept adjacent to its content and siblings on one line
(`<%= render A.new do -%>\n<%= render B.new do -%>\nBody<% end %><% end %>`),
and a break inside a tag (`<%=\n render … %>` … `<%\n end %>`).

**Decision.** Every ERB fixture under `gem/test/golden/views/` is one line
plus a trailing newline, as Button's 15 already were. This is a convention,
chosen because one mechanical shape is the one a reviewer can compare with
the scenario block without thinking, and neither whitespace-tight multi-line
layout reads better than one line for a fixture. The suite proves a fixture's
*output*, not its faithfulness — an argument the component ignores renders
identical HTML — so comparing each fixture with its scenario block stays part
of review. The inside-tag form and the adjacent-`end` form are the candidates
for the sidecars, where users read and edit the file; plan 2.1 chooses when it
writes the first one, measures again under whatever Herb it pins, and records
the choice here.

**Cost if wrong:** six composite fixtures between 700 and 1,726 characters
on one line (`data_table/full_frame` is the longest). **What would reverse
it:** Herb honouring `-%>` on a block-closing `end` *and* dropping the
indentation before an output tag — both, since either alone still leaks
whitespace — at which point trim mode is enough and decision 8 stands as
written.
5 changes: 5 additions & 0 deletions gem/test/golden/harness.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ def render(&block)
# The ERB lane: renders a scenario's fixture through the same harness a
# host application's templates go through, with the same pins active.
def render_erb(scenario)
# ActionView instruments the render, and the first instrumentation on a
# thread creates the Instrumenter, whose id is SecureRandom.hex(10).
# Inside the pin that call would take the counter's first value and
# shift every generated id in that one render by one. Create it first.
ActiveSupport::Notifications.instrumenter

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The fix makes the Instrumenter deterministic on a single thread, but the pin state it relies on is not thread-local: @active, @hex_calls, and @rand_calls are instance variables on the Golden::Harness module, shared by every thread and reset by each render. If two threads render concurrently (the fresh-thread test in gem/test/golden/harness_test.rb is the first code path that exercises threads), the second thread's ActiveSupport::Notifications.instrumenter call can run while another thread has @active == true; its SecureRandom.hex(10) then calls next_hex and consumes a shared counter slot, shifting the ids of the in-flight render and of the new render. Each render also rewrites the shared counters and toggles @active, so one thread's ensure turns the pin off while another thread is still rendering — letting real randomness leak into a pinned render. The claimed "identical output on a fresh thread" property therefore holds only when renders across threads never overlap. Make the counters and @active thread-local (Thread.current) or serialize renders with a Mutex, or document that the harness is single-threaded-only.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/test/golden/harness.rb, line 39:

<comment>The fix makes the Instrumenter deterministic on a single thread, but the pin state it relies on is not thread-local: `@active`, `@hex_calls`, and `@rand_calls` are instance variables on the `Golden::Harness` module, shared by every thread and reset by each render. If two threads render concurrently (the fresh-thread test in `gem/test/golden/harness_test.rb` is the first code path that exercises threads), the second thread's `ActiveSupport::Notifications.instrumenter` call can run while another thread has `@active == true`; its `SecureRandom.hex(10)` then calls `next_hex` and consumes a shared counter slot, shifting the ids of the in-flight render and of the new render. Each render also rewrites the shared counters and toggles `@active`, so one thread's `ensure` turns the pin off while another thread is still rendering — letting real randomness leak into a pinned render. The claimed "identical output on a fresh thread" property therefore holds only when renders across threads never overlap. Make the counters and `@active` thread-local (Thread.current) or serialize renders with a Mutex, or document that the harness is single-threaded-only.</comment>

<file context>
@@ -32,6 +32,11 @@ def render(&block)
+        # thread creates the Instrumenter, whose id is SecureRandom.hex(10).
+        # Inside the pin that call would take the counter's first value and
+        # shift every generated id in that one render by one. Create it first.
+        ActiveSupport::Notifications.instrumenter
         @active = true
         @hex_calls = 0
</file context>

@active = true
@hex_calls = 0
@rand_calls = 0
Expand Down
15 changes: 15 additions & 0 deletions gem/test/golden/harness_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

require "test_helper"
require "golden/harness"
require "golden/catalog"
require "golden/scenarios"

class GoldenHarnessTest < Minitest::Test
def test_records_a_class_when_it_renders_not_when_it_is_instantiated
Expand Down Expand Up @@ -52,6 +54,19 @@ def test_pins_rand_to_the_same_value_on_every_render
assert_includes 50..89, values.first
end

# ActionView instruments every render, and the first instrumentation on a
# thread creates the Instrumenter, whose id is SecureRandom.hex(10). A fresh
# thread reproduces "the first ERB render of the process": the pin must not
# hand that call the counter's first value, or every generated id in that one
# render is shifted by one and the scenario fails its own determinism check.
def test_erb_lane_mints_the_same_ids_on_a_threads_first_render
scenario = Golden::Catalog.scenarios.find { |candidate| candidate.slug == "tooltip/default" }
first, second = Thread.new { Array.new(2) { Golden::Harness.render_erb(scenario) } }.value

assert_equal second, first
assert_includes first, 'id="tooltip00000001"'
end

private

# The recording hash is process-wide and the golden scenarios fill it; swap
Expand Down
8 changes: 8 additions & 0 deletions gem/test/golden/scenarios.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
# what users write, and for table/list/select fragments it is also what
# keeps the scenario meaningful rather than a bare `<td>`.
#
# Every scenario also has an ERB fixture at
# test/golden/views/<component>/<name>.html.erb: the same composition written
# as `<%= render RubyUI::X.new(...) do %>...<% end %>`, on one line, because
# the strict lane sees every newline a fixture adds and Herb's trim mode keeps
# the one after an `end` that follows content (decisions 7, 8 and 10). Both lanes
# compare against the same snapshots; while a scenario keeps its Phlex block,
# that lane records and the ERB lane compares.
#
# Variant coverage is enumerative where a component exposes a closed set:
# Button and Link have six variants and four sizes, Badge has 28 colours, and
# those are the cases a restyle regresses.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Accordion.new do %><%= render RubyUI::AccordionItem.new(open: true, rotate_icon: 90) do %><%= render RubyUI::AccordionTrigger.new do %><p>What is RubyUI?</p><%= render RubyUI::AccordionIcon.new %><% end %><%= render RubyUI::AccordionContent.new do %><p>A UI component library for Ruby.</p><% end %><% end %><% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Accordion.new do %><%= render RubyUI::AccordionItem.new do %><%= render RubyUI::AccordionDefaultTrigger.new do %>Title<% end %><%= render RubyUI::AccordionDefaultContent.new do %>Content<% end %><% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/alert/default.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Alert.new do %><%= render RubyUI::AlertTitle.new do %>Heads up!<% end %><%= render RubyUI::AlertDescription.new do %>You can add components to your app.<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/alert/destructive.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Alert.new(variant: :destructive) do %><%= render RubyUI::AlertTitle.new do %>Error<% end %><%= render RubyUI::AlertDescription.new do %>Your session expired.<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/alert/success.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Alert.new(variant: :success) do %><%= render RubyUI::AlertTitle.new do %>Done<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/alert/warning.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Alert.new(variant: :warning) do %><%= render RubyUI::AlertTitle.new do %>Careful<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/alert_dialog/default.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::AlertDialog.new do %><%= render RubyUI::AlertDialogTrigger.new do %><%= render RubyUI::Button.new do %>Show dialog<% end %><% end %><%= render RubyUI::AlertDialogContent.new do %><%= render RubyUI::AlertDialogHeader.new do %><%= render RubyUI::AlertDialogTitle.new do %>Are you absolutely sure?<% end %><%= render RubyUI::AlertDialogDescription.new do %>This action cannot be undone.<% end %><% end %><%= render RubyUI::AlertDialogFooter.new do %><%= render RubyUI::AlertDialogCancel.new do %>Cancel<% end %><%= render RubyUI::AlertDialogAction.new do %>Continue<% end %><% end %><% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/alert_dialog/open.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::AlertDialog.new(open: true) do %><%= render RubyUI::AlertDialogContent.new do %><%= render RubyUI::AlertDialogTitle.new do %>Open<% end %><% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/aspect_ratio/default.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::AspectRatio.new do %><img alt="Placeholder" loading="lazy" src="/placeholder.png"><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/aspect_ratio/square.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::AspectRatio.new(aspect_ratio: "1/1", class: "rounded-md border") do %><img alt="Placeholder" src="/placeholder.png"><% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Avatar.new do %><%= render RubyUI::AvatarImage.new(src: "/avatar.png", alt: "Jane Doe") %><%= render RubyUI::AvatarFallback.new do %>JD<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/avatar/size_lg.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Avatar.new(size: :lg) do %><%= render RubyUI::AvatarFallback.new do %>JD<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/avatar/size_md.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Avatar.new(size: :md) do %><%= render RubyUI::AvatarFallback.new do %>JD<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/avatar/size_sm.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Avatar.new(size: :sm) do %><%= render RubyUI::AvatarFallback.new do %>JD<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/avatar/size_xl.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Avatar.new(size: :xl) do %><%= render RubyUI::AvatarFallback.new do %>JD<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/badge/all_variants.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<% RubyUI::Badge::COLORS.each_key do |variant| %><%= render RubyUI::Badge.new(variant: variant) do %><%= variant %><% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/badge/size_lg.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Badge.new(size: :lg) do %>Badge<% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/badge/size_md.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Badge.new(size: :md) do %>Badge<% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/badge/size_sm.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Badge.new(size: :sm) do %>Badge<% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/breadcrumb/default.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Breadcrumb.new do %><%= render RubyUI::BreadcrumbList.new do %><%= render RubyUI::BreadcrumbItem.new do %><%= render RubyUI::BreadcrumbLink.new(href: "/") do %>Home<% end %><% end %><%= render RubyUI::BreadcrumbSeparator.new %><%= render RubyUI::BreadcrumbItem.new do %><%= render RubyUI::BreadcrumbEllipsis.new %><% end %><%= render RubyUI::BreadcrumbSeparator.new %><%= render RubyUI::BreadcrumbItem.new do %><%= render RubyUI::BreadcrumbPage.new do %>Current<% end %><% end %><% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/bubble/content_as_anchor.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Bubble.new do %><%= render RubyUI::BubbleContent.new(as: :a, href: "#") do %>Open<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/bubble/default.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Bubble.new do %><%= render RubyUI::BubbleContent.new do %>Hi<% end %><% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::BubbleGroup.new do %><%= render RubyUI::Bubble.new(variant: :muted, align: :end) do %><%= render RubyUI::BubbleContent.new do %>Hi<% end %><%= render RubyUI::BubbleReactions.new do %>OK<% end %><% end %><% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::BubbleReactions.new(side: :top, align: :start) do %>OK<% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/calendar/bound_to_input.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Calendar.new(input_id: "#event-date", selected_date: "2026-05-15", min_date: "2026-05-07", date_format: "dd/MM/yyyy", class: "rounded-md border shadow") %>
1 change: 1 addition & 0 deletions gem/test/golden/views/calendar/default.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Calendar.new %>
1 change: 1 addition & 0 deletions gem/test/golden/views/calendar/header_parts.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::CalendarHeader.new do %><%= render RubyUI::CalendarTitle.new(default: "May 2026") %><%= render RubyUI::CalendarPrev.new %><%= render RubyUI::CalendarNext.new %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/card/default.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Card.new do %><%= render RubyUI::CardHeader.new do %><%= render RubyUI::CardTitle.new do %>Create project<% end %><%= render RubyUI::CardDescription.new do %>Deploy your new project in one click.<% end %><% end %><%= render RubyUI::CardContent.new do %>Body<% end %><%= render RubyUI::CardFooter.new do %><%= render RubyUI::Button.new do %>Deploy<% end %><% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/carousel/horizontal.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Carousel.new(orientation: :horizontal) do %><%= render RubyUI::CarouselContent.new do %><%= render RubyUI::CarouselItem.new do %>1<% end %><%= render RubyUI::CarouselItem.new do %>2<% end %><% end %><%= render RubyUI::CarouselPrevious.new %><%= render RubyUI::CarouselNext.new %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/carousel/vertical.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Carousel.new(orientation: :vertical) do %><%= render RubyUI::CarouselContent.new do %><%= render RubyUI::CarouselItem.new do %>1<% end %><%= render RubyUI::CarouselItem.new do %>2<% end %><% end %><%= render RubyUI::CarouselPrevious.new %><%= render RubyUI::CarouselNext.new %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/carousel/with_options.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Carousel.new(options: {loop: true, align: "start"}) do %><%= render RubyUI::CarouselContent.new do %><%= render RubyUI::CarouselItem.new do %>1<% end %><% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/chart/bar.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Chart.new(options: {type: "bar", data: {labels: ["Phlex", "ERB"], datasets: [{label: "render time (ms)", data: [100, 520]}]}, options: {indexAxis: "y", scales: {y: {beginAtZero: true}}}}) %>
1 change: 1 addition & 0 deletions gem/test/golden/views/checkbox/checked_disabled.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Checkbox.new(name: "terms", checked: true, disabled: true) %>
1 change: 1 addition & 0 deletions gem/test/golden/views/checkbox/default.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Checkbox.new(name: "terms", value: "1") %>
1 change: 1 addition & 0 deletions gem/test/golden/views/checkbox/group.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::CheckboxGroup.new do %><%= render RubyUI::Checkbox.new(name: "colors[]", value: "red") %><%= render RubyUI::Checkbox.new(name: "colors[]", value: "blue") %><% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Clipboard.new(success: "Copiado!", error: "Falhou!") do %><%= render RubyUI::ClipboardSource.new do %>gem install ruby_ui<% end %><%= render RubyUI::ClipboardTrigger.new do %><%= render RubyUI::Button.new(icon: true) do %>C<% end %><% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/clipboard/default.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Clipboard.new %>
1 change: 1 addition & 0 deletions gem/test/golden/views/clipboard/popover_error.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::ClipboardPopover.new(type: :error) do %>error<% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/clipboard/popover_success.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::ClipboardPopover.new(type: :success) do %>success<% end %>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This fixture renders a ClipboardPopover whose panel is hidden at initial render (parent div has class="hidden"), but the recorded snapshot freezes the panel in data-state="open". ContextMenu, HoverCard, and Popover all rest hidden panels in data-state="closed" (see context_menu_content.rb, hover_card_content.rb, popover_content.rb), and starting "open" leaves the animate-in classes active at rest when the panel is not visible. Since this PR records the golden snapshot for this scenario, set ClipboardPopover#default_attrs state to :closed so hidden popovers start closed, then re-record the snapshot.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/test/golden/views/clipboard/popover_success.html.erb, line 1:

<comment>This fixture renders a ClipboardPopover whose panel is hidden at initial render (parent div has class="hidden"), but the recorded snapshot freezes the panel in data-state="open". ContextMenu, HoverCard, and Popover all rest hidden panels in data-state="closed" (see context_menu_content.rb, hover_card_content.rb, popover_content.rb), and starting "open" leaves the animate-in classes active at rest when the panel is not visible. Since this PR records the golden snapshot for this scenario, set ClipboardPopover#default_attrs state to :closed so hidden popovers start closed, then re-record the snapshot.</comment>

<file context>
@@ -0,0 +1 @@
+<%= render RubyUI::ClipboardPopover.new(type: :success) do %>success<% end %>
</file context>

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Codeblock.new("def hello_world\n puts \"Hello, world!\"\nend\n", syntax: :ruby) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Codeblock.new("puts :ok\n", syntax: :ruby, clipboard: false) %>
1 change: 1 addition & 0 deletions gem/test/golden/views/collapsible/closed.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Collapsible.new do %><%= render RubyUI::CollapsibleTrigger.new do %><%= render RubyUI::Button.new(variant: :ghost) do %>Toggle<% end %><% end %><%= render RubyUI::CollapsibleContent.new do %>Hidden body<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/collapsible/open.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Collapsible.new(open: true) do %><%= render RubyUI::CollapsibleTrigger.new do %><%= render RubyUI::Button.new(variant: :ghost) do %>Toggle<% end %><% end %><%= render RubyUI::CollapsibleContent.new do %>Visible body<% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/combobox/badge.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::ComboboxBadge.new do %>Rails<% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/combobox/clear_button.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::ComboboxClearButton.new %>
1 change: 1 addition & 0 deletions gem/test/golden/views/combobox/input_trigger.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::ComboboxInputTrigger.new(placeholder: "Pick one") %>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Combobox.new(multiple: true, term: "frameworks", placement: "top-start") do %><%= render RubyUI::ComboboxBadgeTrigger.new(placeholder: "Select", clear_button: true) %><%= render RubyUI::ComboboxPopover.new do %><%= render RubyUI::ComboboxList.new do %><%= render RubyUI::ComboboxToggleAllCheckbox.new %><%= render RubyUI::ComboboxItem.new do %><%= render RubyUI::ComboboxCheckbox.new(name: "Rails", value: "rails") %><%= render RubyUI::ComboboxItemIndicator.new %><% end %><% end %><% end %><% end %>
1 change: 1 addition & 0 deletions gem/test/golden/views/combobox/radio_items.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render RubyUI::Combobox.new(term: "frameworks") do %><%= render RubyUI::ComboboxTrigger.new(placeholder: "Select your framework") %><%= render RubyUI::ComboboxPopover.new do %><%= render RubyUI::ComboboxSearchInput.new(placeholder: "Type the framework name") %><%= render RubyUI::ComboboxList.new do %><%= render RubyUI::ComboboxEmptyState.new do %>No results<% end %><%= render RubyUI::ComboboxListGroup.new(label: "Ruby") do %><%= render RubyUI::ComboboxItem.new do %><%= render RubyUI::ComboboxRadio.new(name: "Rails", value: "rails") %><% end %><%= render RubyUI::ComboboxItem.new do %><%= render RubyUI::ComboboxRadio.new(name: "Hanami", value: "hanami") %><% end %><% end %><% end %><% end %><% end %>
Loading
Loading