From 281574af237f64f74145d4c8ce31df6964f54f31 Mon Sep 17 00:00:00 2001 From: maebeale Date: Tue, 14 Jul 2026 16:45:30 -0400 Subject: [PATCH] Redesign the callout editor cards: Published toggle + Visibility section Reposition the Published checkbox to each card's top-right, cluster the conditional show/hide controls into a collapsible Visibility section (drip date + payment gate) laid out as parallel fields, and move the built-in default status (Matches default / Restore to default) and Delete to the bottom-right. Add RegistrationTicketCallout#payment? so the editor can hide Visibility from the Payment built-in, whose visibility the app drives from live balance status. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/models/registration_ticket_callout.rb | 7 + ...egistration_ticket_callout_fields.html.erb | 178 +++++++++++------- .../registration_ticket_callout_spec.rb | 11 ++ 3 files changed, 127 insertions(+), 69 deletions(-) diff --git a/app/models/registration_ticket_callout.rb b/app/models/registration_ticket_callout.rb index accc0bcee..74e8546b6 100644 --- a/app/models/registration_ticket_callout.rb +++ b/app/models/registration_ticket_callout.rb @@ -140,6 +140,13 @@ def page_content? description.present? || resources.any? end + # The Payment built-in's visibility is driven entirely by live balance status, + # so the manual visibility controls (drip date, payment gate) don't apply — the + # editor hides its Visibility section. + def payment? + magic_key == "payment" + end + # Whether the callout's page content is drip-scheduled to reveal only from a # future date. The card still shows on the ticket; the page withholds its # content until then (see the callout show page). diff --git a/app/views/events/_registration_ticket_callout_fields.html.erb b/app/views/events/_registration_ticket_callout_fields.html.erb index bfd44d681..7163909df 100644 --- a/app/views/events/_registration_ticket_callout_fields.html.erb +++ b/app/views/events/_registration_ticket_callout_fields.html.erb @@ -20,35 +20,41 @@
<% end %> -
- <%= f.check_box :published, class: "peer rounded border-gray-300 text-purple-600" %> - +
<%# - Always-visible summary row: sits inline next to the checkbox so you - know which callout you are toggling even when it is hidden. + Published toggle, pinned to the card's top-right corner. The checkbox is + kept as a bare sibling *before* the content div below so the + `peer-checked:block` reveal still works — only its visual position is moved + (via absolute), not its DOM order. For the same reason the label is a + separate `for`-linked element rather than wrapping the input (wrapping it + would stop it being a sibling of the content div and break the reveal). %> + <%= f.label :published, "Published", class: "absolute top-0 right-6 text-sm font-medium text-gray-600 cursor-pointer select-none" %> + <%= f.check_box :published, class: "peer rounded border-gray-300 text-purple-600 absolute top-0.5 right-0" %> -
<%= f.object.title %>
+ <%# + Always-visible header row: identifies the callout even when its editor is + collapsed. Holds the built-in badge, title, and default-status text on the + left; `pr-24` reserves the top-right gutter so nothing runs under the + Published toggle pinned there. + %> - <%# Editable content — hidden when the Published checkbox is unchecked. %> - - <%# Both collapsible sections share one row so their toggles line up. %> -
-
- - - Type, colour, icon, and display date - - - <%# - Two rows of two: type + colour, then icon + the drip date that gates - when the callout first shows on the ticket. - %> + <%# + Two columns, top-aligned. The left column stacks Type/color/icon and + Visibility so Visibility always sits directly beneath Type/color/icon, + independent of how tall the Linked resources column on the right grows. + %> +
+
+
+ + + Type, color, and icon + + + <%# One row of three: type, color, icon. %> +
+
+ <%= f.label :callout_type, "Type", class: "block text-xs font-medium text-gray-600 mb-0.5" %> + + <%= f.select :callout_type, + RegistrationTicketCallout::CALLOUT_TYPES.map { |t| [ t.humanize, t ] }, + {}, + class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm", + data: { callout_preview_target: "typeSelect", action: "callout-preview#update" } %> +
-
-
- <%= f.label :callout_type, "Type", class: "block text-xs font-medium text-gray-600 mb-0.5" %> +
+ <%= f.label :color_class, "Color", class: "block text-xs font-medium text-gray-600 mb-0.5" %> - <%= f.select :callout_type, - RegistrationTicketCallout::CALLOUT_TYPES.map { |t| [ t.humanize, t ] }, - {}, - class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm", - data: { callout_preview_target: "typeSelect", action: "callout-preview#update" } %> -
+ <%= f.select :color_class, + DomainTheme::SWATCH_COLORS.map { |c| [ c.to_s.humanize, c ] }, + { include_blank: "Default (by type)" }, + class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm", + data: { callout_preview_target: "colorSelect", action: "callout-preview#update" } %> -
- <%= f.label :color_class, "Colour", class: "block text-xs font-medium text-gray-600 mb-0.5" %> + <% if f.object.app_colored? %> +

The app overrides this with a live-status color when the registrant has action to take (e.g. amber while something's outstanding, orange while a balance is due).

+ <% end %> +
- <%= f.select :color_class, - DomainTheme::SWATCH_COLORS.map { |c| [ c.to_s.humanize, c ] }, - { include_blank: "Default (by type)" }, - class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm", - data: { callout_preview_target: "colorSelect", action: "callout-preview#update" } %> +
+ <%= f.label :icon_class, "Icon", class: "block text-xs font-medium text-gray-600 mb-0.5" %> - <% if f.object.app_colored? %> -

The app overrides this with a live-status colour when the registrant has action to take (e.g. amber while something's outstanding, orange while a balance is due).

- <% end %> + <%= f.text_field :icon_class, + class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm font-mono", + data: { callout_preview_target: "iconInput", action: "callout-preview#update" } %> +
+
-
- <%= f.label :icon_class, "Icon", class: "block text-xs font-medium text-gray-600 mb-0.5" %> + <%# + Visibility: the callout's show/hide rules — a drip date and the + payment gate. Opened on load when either is set. Hidden for the Payment + built-in, which the app shows/hides from live balance status. + %> + <% unless f.object.payment? %> +
> + + + Visibility + - <%= f.text_field :icon_class, - class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm font-mono", - data: { callout_preview_target: "iconInput", action: "callout-preview#update" } %> -
+
+
+ <%= f.label :display_from, "Display date", class: "block text-xs font-medium text-gray-600 mb-0.5" %> -
- <%= f.label :display_from, "Display date", class: "block text-xs font-medium text-gray-600 mb-0.5" %> + <%= f.date_field :display_from, + class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm" %> - <%= f.date_field :display_from, - class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm" %> +

Drips onto the ticket on and after this date. Leave blank to show right away.

+
-

Drips onto the ticket on and after this date. Leave blank to show right away.

-
-
-
+
+ <%= f.label :payment_access_gated, "Payment-gated", class: "block text-xs font-medium text-gray-600 mb-0.5" %> + + +
+
+ + <% end %> +
- <%# Opened on load only when resources are saved. %> + <%# RIGHT COLUMN: linked resources. Opened on load only when resources are saved. %>
> @@ -187,10 +217,20 @@
-
- <% unless f.object.magic? %> - - <%= link_to_remove_association "Remove", f, + <%# + Bottom-right action: custom callouts can be deleted; built-ins can't, so + they show their default status (or a restore checkbox once customized) in + the same spot instead. + %> +
+ <% if f.object.magic? %> + <% if f.object.persisted? && DefaultTicketCallouts.customized?(f.object) %> + + <% else %> + Matches default + <% end %> + <% else %> + <%= link_to_remove_association "Delete callout", f, class: "text-sm text-gray-400 hover:text-red-600 underline" %> <% end %>
diff --git a/spec/models/registration_ticket_callout_spec.rb b/spec/models/registration_ticket_callout_spec.rb index d9ddb3e17..3e62bf495 100644 --- a/spec/models/registration_ticket_callout_spec.rb +++ b/spec/models/registration_ticket_callout_spec.rb @@ -69,6 +69,17 @@ expect(event.registration_ticket_callouts.visible).to eq([ shown ]) end + + it "#payment? is true only for the payment built-in" do + event = create(:event) + payment = create(:registration_ticket_callout, event:, magic_key: "payment") + other = create(:registration_ticket_callout, event:, magic_key: "faq") + custom = create(:registration_ticket_callout, event:, magic_key: nil) + + expect(payment.payment?).to be(true) + expect(other.payment?).to be(false) + expect(custom.payment?).to be(false) + end end describe "#published (inverse of hidden)" do