diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb new file mode 100644 index 000000000..fcc259a64 --- /dev/null +++ b/app/helpers/button_helper.rb @@ -0,0 +1,45 @@ +module ButtonHelper + # Single source of truth for button styling, replacing the @apply-based .btn + # component classes (Evil Martians best practice #4: keep the utilities in the + # markup via a helper rather than extracting them into @apply CSS). Tailwind + # scans this file (see the @source in application.tailwind.css), so the class + # strings below generate exactly like inline utilities. + BUTTON_BASE = "inline-flex items-center gap-2 rounded-lg font-medium shadow-sm " \ + "transition-colors duration-200 focus:outline-none focus:ring-2 " \ + "focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed".freeze + + # Size sets the padding + text scale. Pass `size: nil` when the call site + # supplies its own padding/text-size via `extra:` so the two don't collide + # (conflicting utilities in one class list resolve by Tailwind's generation + # order, not markup order — unlike the old component/utility layer split). + BUTTON_SIZES = { + md: "px-4 py-2 text-sm", + sm: "px-3 py-1 text-xs" + }.freeze + + BUTTON_VARIANTS = { + primary: "border border-primary bg-primary text-white hover:bg-white hover:text-primary", + accent: "border-2 border-accent bg-accent text-white hover:bg-white hover:text-accent", + success: "border-2 border-success bg-success text-white hover:bg-white hover:text-success", + secondary: "border border-secondary bg-secondary text-white hover:bg-white hover:text-secondary", + info: "border border-info bg-info text-white hover:bg-white hover:text-info", + warning: "border border-warning bg-warning text-white hover:bg-white hover:text-warning", + danger: "border border-danger bg-danger text-white hover:bg-white hover:text-danger", + utility: "border border-gray-200 bg-gray-200 text-gray-800 hover:bg-white hover:text-gray-600", + primary_outline: "border border-primary text-primary hover:bg-primary hover:text-white", + accent_outline: "border-2 border-accent text-accent hover:bg-accent hover:text-white", + success_outline: "border-2 border-success text-success hover:bg-success hover:text-white", + secondary_outline: "border border-secondary text-secondary hover:bg-secondary hover:text-white", + info_outline: "border border-info text-info hover:bg-info hover:text-white", + warning_outline: "border border-warning text-warning hover:bg-warning hover:text-white", + danger_outline: "border border-danger text-danger hover:bg-danger hover:text-white", + utility_outline: "border border-gray-200 text-gray-600 hover:bg-gray-200 hover:text-gray-800" + }.freeze + + def button_classes(variant = :primary, size: :md, extra: nil) + tokens = [ BUTTON_BASE, BUTTON_VARIANTS.fetch(variant) ] + tokens << BUTTON_SIZES.fetch(size) if size + tokens << extra if extra.present? + tokens.join(" ") + end +end diff --git a/app/views/events/_card.html.erb b/app/views/events/_card.html.erb index ebac6bb15..0cc9274f4 100644 --- a/app/views/events/_card.html.erb +++ b/app/views/events/_card.html.erb @@ -55,7 +55,7 @@ <% if registration&.slug.present? %> <%= link_to "View ticket", registration_ticket_path(registration.slug), data: { turbo_frame: "_top" }, - class: "btn btn-success px-3 py-2 text-xs uppercase leading-tight font-telefon" %> + class: button_classes(:success, size: nil, extra: "px-3 py-2 text-xs uppercase leading-tight font-telefon") %> <% end %> <% elsif event.ended? %> Event ended @@ -67,18 +67,18 @@ <%= button_to "Register", event_registrant_registration_path(event_id: event), data: { turbo: !event.object.cost_cents.to_i.positive? }, - class: "btn btn-accent-outline px-3 py-2 text-xs uppercase leading-tight font-telefon" %> + class: button_classes(:accent_outline, size: nil, extra: "px-3 py-2 text-xs uppercase leading-tight font-telefon") %> <% else %> <%= link_to "Register", new_event_public_registration_path(event), data: { turbo_frame: "_top" }, - class: "btn btn-accent-outline px-3 py-2 text-xs uppercase leading-tight font-telefon" %> + class: button_classes(:accent_outline, size: nil, extra: "px-3 py-2 text-xs uppercase leading-tight font-telefon") %> <% end %> <% elsif event.object.public_registration_enabled? && event.object.event_forms.registration.exists? %> <%= link_to "Register", new_event_public_registration_path(event), data: { turbo_frame: "_top" }, - class: "btn btn-accent-outline px-3 py-2 text-xs uppercase leading-tight font-telefon" %> + class: button_classes(:accent_outline, size: nil, extra: "px-3 py-2 text-xs uppercase leading-tight font-telefon") %> <% end %> <% else %> Registration closed diff --git a/app/views/events/_registration_section.html.erb b/app/views/events/_registration_section.html.erb index e71a8b9e3..935006d5f 100644 --- a/app/views/events/_registration_section.html.erb +++ b/app/views/events/_registration_section.html.erb @@ -11,7 +11,7 @@ <% if allowed_to?(:manage?, event) %> <%= button_to "Register", event_registrant_registration_path(event_id: event), - class: "admin-only bg-blue-100 btn btn-primary-outline" %> + class: button_classes(:primary_outline, extra: "admin-only bg-blue-100") %> <% end %> <% elsif !registered && !slug_registered %> <% if slug_cancelled && event.registerable? %> @@ -25,18 +25,18 @@ <%= button_to button_text, event_registrant_registration_path(event_id: event), data: { turbo: !event.object.cost_cents.to_i.positive? }, - class: "btn btn-accent px-10 py-2 text-2xl uppercase", + class: button_classes(:accent, size: nil, extra: "px-10 py-2 text-2xl uppercase"), style: "font-family: 'Telefon Bold', sans-serif;" %> <% else %> <%= link_to button_text, new_event_public_registration_path(event), - class: "btn btn-accent px-10 py-2 text-2xl uppercase", + class: button_classes(:accent, size: nil, extra: "px-10 py-2 text-2xl uppercase"), style: "font-family: 'Telefon Bold', sans-serif;" %> <% end %> <% elsif event.object.public_registration_enabled? && event.object.event_forms.registration.exists? %> <%= link_to button_text, new_event_public_registration_path(event), - class: "btn btn-accent px-10 py-2 text-2xl uppercase", + class: button_classes(:accent, size: nil, extra: "px-10 py-2 text-2xl uppercase"), style: "font-family: 'Telefon Bold', sans-serif;" %> <% end %> <% else %> @@ -47,11 +47,11 @@ <% if registered %> <% registration = event.active_registration_for(current_user&.person) %> <%= link_to "View ticket", registration_ticket_path(registration.slug), - class: "btn btn-success px-10 py-2 text-2xl uppercase", + class: button_classes(:success, size: nil, extra: "px-10 py-2 text-2xl uppercase"), style: "font-family: 'Telefon Bold', sans-serif;" %> <% elsif slug_registered %> <%= link_to "View ticket", registration_ticket_path(slug_registration.slug), - class: "btn btn-success px-10 py-2 text-2xl uppercase", + class: button_classes(:success, size: nil, extra: "px-10 py-2 text-2xl uppercase"), style: "font-family: 'Telefon Bold', sans-serif;" %> <% end %> diff --git a/app/views/events/callouts/scholarship.html.erb b/app/views/events/callouts/scholarship.html.erb index a7f3fcb94..f2c6f4031 100644 --- a/app/views/events/callouts/scholarship.html.erb +++ b/app/views/events/callouts/scholarship.html.erb @@ -83,13 +83,13 @@ <%# Native
; while it's open :has() hides Agree so the decline form stands alone. %>
<%= form_with url: registration_scholarship_agreement_path(@event_registration.slug), method: :post, class: "group-has-[[open]]:hidden" do %> - <% end %>
- + "> Decline <%= form_with url: registration_scholarship_decline_path(@event_registration.slug), method: :post, class: "mt-3 w-full max-w-md" do %> diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index d6b97dafc..2f3af0f12 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -12,7 +12,7 @@ <% if @preview %>
Preview — unsaved changes - +
<% else %> @@ -110,7 +110,7 @@
<%= link_to Form::BULK_PAYMENT_PUBLIC_NAME, new_event_bulk_payment_path(@event), - class: "btn btn-success px-10 py-2 text-2xl uppercase", + class: button_classes(:success, size: nil, extra: "px-10 py-2 text-2xl uppercase"), style: "font-family: 'Telefon Bold', sans-serif;" %>
<% end %> @@ -131,6 +131,6 @@ <% if @preview %>
Preview — unsaved changes - +
<% end %> diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb new file mode 100644 index 000000000..e48709442 --- /dev/null +++ b/spec/helpers/button_helper_spec.rb @@ -0,0 +1,39 @@ +require "rails_helper" + +RSpec.describe ButtonHelper, type: :helper do + describe "#button_classes" do + it "combines the base, default size, and the requested variant" do + result = helper.button_classes(:primary) + + expect(result).to include("inline-flex", "rounded-lg", "shadow-sm") + expect(result).to include("px-4", "py-2", "text-sm") + expect(result).to include("bg-primary", "hover:text-primary") + end + + it "defaults to the primary variant" do + expect(helper.button_classes).to eq(helper.button_classes(:primary)) + end + + it "swaps in the compact size" do + result = helper.button_classes(:secondary_outline, size: :sm) + + expect(result).to include("px-3", "py-1", "text-xs") + expect(result).not_to include("px-4", "py-2", "text-sm") + end + + it "omits size utilities when size is nil so a call site can supply its own" do + result = helper.button_classes(:success, size: nil, extra: "px-10 py-2 text-2xl") + + expect(result).not_to include("px-4", "text-sm") + expect(result).to include("px-10", "text-2xl") + end + + it "appends extra classes" do + expect(helper.button_classes(:primary, extra: "whitespace-nowrap")).to include("whitespace-nowrap") + end + + it "raises for an unknown variant so typos fail loudly" do + expect { helper.button_classes(:nope) }.to raise_error(KeyError) + end + end +end