diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fe03b440bf..964f046150 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -274,6 +274,7 @@ Follow the [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introducti ## PRs +- **"Add prefix X" means prefix the PR title with `X`** — when the user says "add prefix" (e.g. "add prefix MAYBE:"), prepend that literal string to the current PR's title via `gh pr edit --title`, preserving the rest of the title. It always refers to the PR title, not a commit message or file content. - **Always create PRs as drafts** — every PR starts in draft (`gh pr create --draft`), no exceptions. Never open a PR ready for review, and never promote it. Only the user runs `gh pr ready`, manually and intentionally, when they decide the work is ready. - **Push to a draft PR early** — create the draft PR as soon as work begins, rather than keeping changes in a local branch. Push on every commit. - **In a new Conductor workspace, do this immediately** — as the first step of any task, make an initial commit on the workspace branch and open the draft PR right away (before the work is done), then keep pushing on every commit as you go. Don't wait until there's a finished change to show. diff --git a/AGENTS.md b/AGENTS.md index cf151739df..2818984914 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -399,9 +399,12 @@ end ### Tailwind Theme -Custom colors defined in `app/frontend/stylesheets/application.tailwind.css`: +Custom tokens defined in the `@theme` block of `app/frontend/stylesheets/application.tailwind.css`: - `--color-primary: #063b8d` (dark blue) - Standard semantic colors: secondary, danger, warning, info, success +- Sub-`xs` font sizes for dense UI: `text-2xs` (0.625rem) and `text-3xs` (0.55rem) — use these instead of arbitrary `text-[…px]` values + +Tailwind class order is sorted with rustywind via `ai/tw-sort` (Prettier's Tailwind plugin can't parse ERB). Prefer named theme tokens and static class literals over arbitrary/interpolated classes. ## Testing diff --git a/CLAUDE.md b/CLAUDE.md index 15dd1da027..783edf50d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -284,6 +284,7 @@ Follow the [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introducti ## PRs +- **"Add prefix X" means prefix the PR title with `X`** — when the user says "add prefix" (e.g. "add prefix MAYBE:"), prepend that literal string to the current PR's title via `gh pr edit --title`, preserving the rest of the title. It always refers to the PR title, not a commit message or file content. - **Always create PRs as drafts** — every PR starts in draft (`gh pr create --draft`), no exceptions. Never open a PR ready for review, and never promote it. Only the user runs `gh pr ready`, manually and intentionally, when they decide the work is ready. - **Push to a draft PR early** — create the draft PR as soon as work begins, rather than keeping changes in a local branch. Push on every commit. - **In a new Conductor workspace, do this immediately** — as the first step of any task, make an initial commit on the workspace branch and open the draft PR right away (before the work is done), then keep pushing on every commit as you go. Don't wait until there's a finished change to show. @@ -384,6 +385,7 @@ See `ai/` directory for executable scripts: | `ai/test_extra [args]` | Full RSpec run: Vite test build + all system specs | | `ai/lint` | Rubocop on all files | | `ai/lint --fix` | Auto-fix lint issues | +| `ai/tw-sort` | Sort Tailwind class order (rustywind) on files changed vs main; `--all` for the whole tree, `--check` to verify without writing | | `ai/server` | Start dev services (web + vite) | | `ai/console` | Rails console | | `ai/routes -g pattern` | Search Rails routes | diff --git a/ai/README.md b/ai/README.md index 8b704d2059..560f0018dc 100644 --- a/ai/README.md +++ b/ai/README.md @@ -10,6 +10,7 @@ Quick-reference scripts for common development tasks. Designed for AI agents and | `ai/test_extra [args]` | Full RSpec run: Vite test build + all the headless-Chrome system specs `ai/test` narrows | | `ai/lint` | Rubocop on all files | | `ai/lint --fix` | Auto-fix lint issues | +| `ai/tw-sort` | Sort Tailwind utility-class order with rustywind. Defaults to files changed vs `origin/main` (gradual convergence); `--all` sorts the whole tree, `--check` verifies without writing (non-zero exit if any file is unsorted) | | `ai/server` | Start all dev services (web + vite) | | `ai/console` | Rails console | | `ai/routes -g pattern` | Search Rails routes | diff --git a/ai/tw-sort b/ai/tw-sort new file mode 100755 index 0000000000..411c78102f --- /dev/null +++ b/ai/tw-sort @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Sort Tailwind utility classes into canonical order with rustywind. +# +# ERB is where nearly all our classes live, and Prettier (with +# prettier-plugin-tailwindcss) can't parse ERB — so we use rustywind, which +# reorders only the class-attribute tokens in place and leaves interpolations +# (<%= ... %>) and the rest of the markup untouched. +# +# Usage: +# ai/tw-sort Sort files changed vs origin/main (gradual convergence) +# ai/tw-sort --check Check those files; non-zero exit if any need sorting +# ai/tw-sort --all Sort the whole tree (app/views + app/frontend/javascript) +# ai/tw-sort --all --check +set -euo pipefail +source "$(dirname "$0")/.ruby-env" + +rustywind="$(dirname "$0")/../node_modules/.bin/rustywind" + +mode="write" +scope="changed" +for arg in "$@"; do + case "$arg" in + --check) mode="check" ;; + --all) scope="all" ;; + *) echo "unknown option: $arg" >&2; exit 2 ;; + esac +done + +flag="--write" +[[ "$mode" == "check" ]] && flag="--check-formatted" + +if [[ "$scope" == "all" ]]; then + exec "$rustywind" "$flag" app/views app/frontend/javascript +fi + +# Changed files only: everything modified vs origin/main plus the working tree. +files=() +while IFS= read -r f; do + [[ -n "$f" && -f "$f" ]] && files+=("$f") +done < <( + { git diff --name-only --diff-filter=ACMR origin/main...HEAD + git diff --name-only --diff-filter=ACMR + git diff --name-only --diff-filter=ACMR --cached + } | sort -u | grep -E '\.(erb|js)$' || true +) + +if [[ ${#files[@]} -eq 0 ]]; then + echo "No changed .erb/.js files to sort." + exit 0 +fi + +exec "$rustywind" "$flag" "${files[@]}" diff --git a/app/frontend/stylesheets/application.tailwind.css b/app/frontend/stylesheets/application.tailwind.css index a40cce757d..4e8f93410f 100644 --- a/app/frontend/stylesheets/application.tailwind.css +++ b/app/frontend/stylesheets/application.tailwind.css @@ -17,6 +17,13 @@ --color-success: var(--color-green-500); --color-transparent: transparent; --font-telefon: "Telefon Bold", sans-serif; + + /* Sub-`text-xs` sizes for dense UI (badges, pills, stat rows). Named tokens + replacing scattered arbitrary font-size values so the tiny type stays + consistent and is adjustable in one place. 2xs (0.625rem/10px) consolidates + the ~10px cluster; 3xs (0.55rem) was the next size down. */ + --text-2xs: 0.625rem; + --text-3xs: 0.55rem; } /* Font face declarations */ diff --git a/app/helpers/organization_helper.rb b/app/helpers/organization_helper.rb index 659ff0f11d..20769ee632 100644 --- a/app/helpers/organization_helper.rb +++ b/app/helpers/organization_helper.rb @@ -56,7 +56,7 @@ def organization_profile_button(organization, truncate_at: nil, subtitle: nil, l label_tag = if label.present? content_tag(:span, label, - class: "absolute top-1 right-1 px-1.5 py-0.5 rounded text-[10px] font-semibold bg-gray-200 text-gray-600") + class: "absolute top-1 right-1 px-1.5 py-0.5 rounded text-2xs font-semibold bg-gray-200 text-gray-600") else "".html_safe end diff --git a/app/views/admin/ahoy_activities/charts.html.erb b/app/views/admin/ahoy_activities/charts.html.erb index ad9ba9eaf8..059a204126 100644 --- a/app/views/admin/ahoy_activities/charts.html.erb +++ b/app/views/admin/ahoy_activities/charts.html.erb @@ -1,6 +1,6 @@ <% content_for(:page_bg_class, "admin-only bg-blue-100") %> -
| <% days.each do |d| %> - | <%= d %> | +<%= d %> | <% end %>|||
|---|---|---|---|---|---|
| <%= "#{hour}:00" %> | +<%= "#{hour}:00" %> | <% (1..7).each do |dow| %> <% val = @heatmap_data[[hour, dow]] || 0 %> <% opacity = (val.to_f / max_val).round(2) %> -
@@ -269,9 +269,9 @@
- Workshop analytics-
+
+ Workshop analytics+
<%= chart_card("Workshop search: category types") { bar_chart(@ws_category_types) } %>
<%= chart_card("Workshop search: categories") { bar_chart(@ws_category_names) } %>
<%= chart_card("Workshop search: sectors") { bar_chart(@ws_sectors) } %>
@@ -286,9 +286,9 @@
- Resource analytics-
+
+ Resource analytics+
<%= chart_card("Resource search: keywords") { bar_chart(@rs_keywords) } %>
<%= chart_card("Resource search: kinds") { bar_chart(@rs_kinds) } %>
<%= chart_card("Resource discovery funnel") { column_chart(@rs_funnel) } %>
@@ -297,27 +297,27 @@
- Tag analytics-
+
+ Tag analytics+
<%= link_to tags_path, class: "group relative bg-white border border-gray-200 rounded-xl shadow-sm p-6 hover:bg-blue-50 hover:border-blue-300 hover:shadow-md transition-all" do %>
-
- Tags page views+ +Tags page views
-
<% end %>
<%= link_to taggings_path, class: "group relative bg-white border border-gray-200 rounded-xl shadow-sm p-6 hover:bg-blue-50 hover:border-blue-300 hover:shadow-md transition-all" do %>
-
- <%= number_with_delimiter(@tags_page_views) %>
+ <%= number_with_delimiter(@tags_page_views) %>
Taggings page views+ +Taggings page views
-
<% end %>
- <%= number_with_delimiter(@taggings_page_views) %>
+ <%= number_with_delimiter(@taggings_page_views) %>
- Tagging searches+
+ Tagging searches<%= number_with_delimiter(@tagging_searches) %>
- Top filter combinations+
+ Top filter combinations
- Content discovery-
+
+ Content discovery+
<%= chart_card("Content types people view most") do %>
<%= pie_chart(
scoped_events
@@ -376,33 +376,33 @@
- Content creation-
-
- Ideas submitted+
+ Content creation+
+
+
- Ideas submitted<%= number_with_delimiter(@ideas_submitted) %>
- Ideas promoted+
+
- Ideas promoted<%= number_with_delimiter(@ideas_promoted) %>
- Converted to published content
+ Converted to published content
- Avg ideas per person+
+
- Avg ideas per person<%= @avg_ideas_per_person %>
- Total content created+
+
- Total content created
- User-generated content+
+
- User-generated content
- Admin-generated content+
+ Admin-generated content
- Referrals & technical-
+
+ Referrals & technical+
<%= chart_card("Top referrer → landing pages") do %>
<%= bar_chart @top_referrer_landing, library: { indexAxis: "y" } %>
<% end %>
diff --git a/app/views/admin/analytics/index.html.erb b/app/views/admin/analytics/index.html.erb
index fd3faa9e66..d44250f51e 100644
--- a/app/views/admin/analytics/index.html.erb
+++ b/app/views/admin/analytics/index.html.erb
@@ -1,6 +1,6 @@
<% content_for(:page_bg_class, "admin-only bg-blue-100") %>
-
-
+
+
-
<% if allowed_to?(:index?, with: Admin::HomePolicy) %>
<%= link_to admin_path, class: "inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700" do %>
@@ -12,15 +12,15 @@
- Counts+
+
- Counts
+
<%= render "admin/shared/active_filters_subheader" %>
- Filters+Filters<%= form_with url: admin_activities_counts_path, method: :get, local: true, class: "flex items-center gap-4" do |f| %> <%= render "admin/shared/audience_dropdown" %> @@ -44,22 +44,21 @@
-
+ View counts-
+
<% @summary.each do |label, data| %>
<%= link_to polymorphic_path(label.to_s.classify.constantize), class: "block group" do %>
-
-
-
+
+
+
<%= label.to_s.humanize %>
-
+
<%= number_with_delimiter(data[:views]) %>
@@ -79,9 +78,9 @@
- Workshops-
+
+ Workshops+
<%= render "admin/analytics/popular_list",
title: "Most viewed",
records: @most_viewed_workshops,
@@ -102,9 +101,9 @@
- Resources-
+
+ Resources+
<%= render "admin/analytics/popular_list",
title: "Most viewed",
records: @most_viewed_resources,
@@ -128,9 +127,9 @@
- <%= CommunityNews.model_name.human.pluralize %>-
+
+ <%= CommunityNews.model_name.human.pluralize %>+
<%= render "admin/analytics/popular_list",
title: "Most viewed",
records: @most_viewed_community_news,
@@ -151,9 +150,9 @@
- Stories-
+
+ Stories+
<%= render "admin/analytics/popular_list",
title: "Most viewed",
records: @most_viewed_stories,
@@ -174,9 +173,9 @@
- Other-
+
+ Other+
<%= render "admin/analytics/popular_list",
title: "Events",
records: @most_viewed_events,
@@ -230,12 +229,12 @@
-
diff --git a/app/views/events/_breakdown_card.html.erb b/app/views/events/_breakdown_card.html.erb
index 9c1c538f1b..3fa581c30b 100644
--- a/app/views/events/_breakdown_card.html.erb
+++ b/app/views/events/_breakdown_card.html.erb
@@ -56,7 +56,7 @@
<% if path %>onclick="window.location='<%= path %>'"<% end %>>
+ No engagement-
+
<%= render "zero_list",
title: "Workshops",
records: @zero_engagement_workshops,
diff --git a/app/views/allocations/_allocatable_link.html.erb b/app/views/allocations/_allocatable_link.html.erb
index d1778a06d2..4565a12303 100644
--- a/app/views/allocations/_allocatable_link.html.erb
+++ b/app/views/allocations/_allocatable_link.html.erb
@@ -14,7 +14,7 @@
<%= descriptor[:subtitle] %>
<% end %>
-
+
<% end %>
<% else %>
diff --git a/app/views/allocations/_allocated_to.html.erb b/app/views/allocations/_allocated_to.html.erb
index 8b9034b026..48021a4f3b 100644
--- a/app/views/allocations/_allocated_to.html.erb
+++ b/app/views/allocations/_allocated_to.html.erb
@@ -2,12 +2,12 @@
allocatable is an event registration, the whole block is a button to its
admin page with a jump-link icon in the top-right corner. ---%>
diff --git a/app/views/continuing_education_registrations/_payment_history.html.erb b/app/views/continuing_education_registrations/_payment_history.html.erb
index b96408c4b0..738af1ae03 100644
--- a/app/views/continuing_education_registrations/_payment_history.html.erb
+++ b/app/views/continuing_education_registrations/_payment_history.html.erb
@@ -13,14 +13,14 @@
class: "ml-auto inline-flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:underline",
target: "_blank", rel: "noopener" do %>
View all
-
+
<% end %>
- Allocated to:
+ Allocated to:
<% if allocatable.is_a?(EventRegistration) %>
<%= link_to edit_event_registration_path(allocatable),
class: "relative block rounded-lg border border-gray-300 bg-white px-3 py-2 pr-8 transition-colors hover:border-gray-400 hover:bg-gray-50",
data: { turbo_frame: "_top" } do %>
-
+
Event registration for <%= allocatable.registrant&.full_name %>
<%= allocatable.event.title %> · <%= allocatable.event.decorate.times(display_day: true, display_date: true) %>
<% end %>
diff --git a/app/views/comments/_aggregated_comment.html.erb b/app/views/comments/_aggregated_comment.html.erb
index 146b6b59bd..66f5cb7753 100644
--- a/app/views/comments/_aggregated_comment.html.erb
+++ b/app/views/comments/_aggregated_comment.html.erb
@@ -20,7 +20,7 @@
<%= render "comments/flag_toggle", commentable: comment.commentable, comment: comment.object %>
-
@@ -80,9 +80,9 @@
<%= link_to new_scholarship_path(allocatable_sgid: event_registration.to_sgid.to_s, return_to: "registration"),
class: "inline-flex items-center gap-1.5 self-start rounded-md px-2 py-1 text-xs font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700",
target: "_blank", rel: "noopener" do %>
-
+
Add scholarship
-
+
<% end %>
-
<% end %>
diff --git a/app/views/events/_attendance_day_form.html.erb b/app/views/events/_attendance_day_form.html.erb
index efe9b50394..09b760418e 100644
--- a/app/views/events/_attendance_day_form.html.erb
+++ b/app/views/events/_attendance_day_form.html.erb
@@ -36,6 +36,6 @@
Organization: <%= entry[:org_name].presence || "—" %><% if entry[:organization]&.city_state.present? %> · <%= entry[:organization].city_state %><% end %><%# Mirror the roster's amber "Pending" chip so it's clear why this registrant was flagged — shown only while nothing is linked. %><% if entry[:org_name].present? && @linked_organizations.none? %> <%= render "shared/badge", label: "Pending", classes: "bg-amber-50 text-amber-700 border-amber-200", icon: "fa-solid fa-circle-exclamation text-[0.65rem]", extra: "ml-1", title: "The registrant submitted this organization, but none is linked to the registration yet." %><% end %> +Organization: <%= entry[:org_name].presence || "—" %><% if entry[:organization]&.city_state.present? %> · <%= entry[:organization].city_state %><% end %><%# Mirror the roster's amber "Pending" chip so it's clear why this registrant was flagged — shown only while nothing is linked. %><% if entry[:org_name].present? && @linked_organizations.none? %> <%= render "shared/badge", label: "Pending", classes: "bg-amber-50 text-amber-700 border-amber-200", icon: "fa-solid fa-circle-exclamation text-2xs", extra: "ml-1", title: "The registrant submitted this organization, but none is linked to the registration yet." %><% end %> Position / title: <%= entry[:position].presence || "—" %>
<%= link_to "Cancel", cancel_path, class: "text-xs text-gray-500 hover:text-gray-700" %>
- Leave the sign-out blank to leave a session open.
+ Leave the sign-out blank to leave a session open.
<% end %>
diff --git a/app/views/events/_attendance_sessions.html.erb b/app/views/events/_attendance_sessions.html.erb
index 217d912c19..6770520a81 100644
--- a/app/views/events/_attendance_sessions.html.erb
+++ b/app/views/events/_attendance_sessions.html.erb
@@ -32,7 +32,7 @@
<%= link_to attendance_event_path(@event, **state, edit: cell, anchor: cell),
class: "ml-1 rounded px-1.5 py-0.5 text-xs font-medium text-teal-700 opacity-50 transition-opacity hover:bg-teal-50 hover:underline group-hover:opacity-100 focus:opacity-100",
"aria-label": "Edit #{row.name}'s times for #{date.strftime("%b %-d")}" do %>
- Edit
+ Edit
<% end %>
| |||
| - + | + <% if is_scholarship %> " aria-hidden="true"> <% end %> <% if row[:high_profile] %> - + <% end %> <% if path.present? %> <%= link_to row[:name], path, class: "group-hover:underline align-middle" %> @@ -23,11 +23,11 @@ <%= row[:name] %> <% end %> | -+ | "><%= row[:count] %> · <%= total.zero? ? 0 : (row[:count] * 100.0 / total).round(1) %>% <%# Jump-link affordance: invisible (white) until the row is hovered, matching the other breakdown cards. %> <% if path.present? %> - + <% end %> |
No registrants found.
+No registrants found.
<% end %> <% end %> diff --git a/app/views/events/_registration_ticket_callout_fields.html.erb b/app/views/events/_registration_ticket_callout_fields.html.erb index 95b1d4dfe1..10a0278f0b 100644 --- a/app/views/events/_registration_ticket_callout_fields.html.erb +++ b/app/views/events/_registration_ticket_callout_fields.html.erb @@ -89,7 +89,7 @@ toggles `hidden`). Defaults open when published, closed otherwise; render the matching initial state to avoid a flash before the controller connects. `mt-5` keeps the preview card clear of the controls above it. %> -<%= @event.title %>
<%= link_to @event.title, event_path(@event), class: "hover:underline" %>
-<%= @event.short_date_range %>
+<%= link_to @event.title, event_path(@event), class: "hover:underline" %>
+<%= @event.short_date_range %>
<% if @event.labelled_cost.present? %> -<%= @event.labelled_cost %>
+<%= @event.labelled_cost %>
<% end %><%= payer_name.presence || "—" %>
+<%= payer_name.presence || "—" %>
<% if payer_organization.present? %> -<%= payer_organization %>
+<%= payer_organization %>
<% end %> -+
Covering <%= attendee_count %> <%= "registrant".pluralize(attendee_count) %>
@@ -109,11 +109,11 @@| Name | -Name | +||
|---|---|---|---|
| <%= [ attendee["first_name"], attendee["last_name"] ].compact_blank.join(" ").presence || "—" %> | @@ -155,14 +155,14 @@ <%= link_to event_invoice_path(@event, submission_id: @submission.id, return_to: "bulk_payment_ticket"), class: "flex items-center gap-3 rounded-xl border-2 border-blue-200 bg-blue-50 px-4 py-3 hover:bg-blue-100 transition-colors" do %> - + -
No registrants found.
+No registrants found.
<% end %>