From 780aae811336a6ee1eec068e48eea0520a515323 Mon Sep 17 00:00:00 2001 From: Kai Wagner Date: Thu, 19 Feb 2026 08:16:06 +0100 Subject: [PATCH] Reworked sidebar and added grouping, a chip appears on filter or tag selection that is easily seen and can be removed and no topics matching a filter, gives a clear error message and the ability to clear filters easily Signed-off-by: Kai Wagner --- app/assets/stylesheets/components/sidebar.css | 49 ++++++++++++++- app/assets/stylesheets/components/topics.css | 61 +++++++++++++++++++ app/helpers/topics_helper.rb | 18 ++++++ app/views/topics/_sidebar.html.slim | 58 +++++++++++------- app/views/topics/_topics.html.slim | 9 +++ app/views/topics/index.html.slim | 16 +++-- 6 files changed, 181 insertions(+), 30 deletions(-) diff --git a/app/assets/stylesheets/components/sidebar.css b/app/assets/stylesheets/components/sidebar.css index 207d96d..0dfb6d9 100644 --- a/app/assets/stylesheets/components/sidebar.css +++ b/app/assets/stylesheets/components/sidebar.css @@ -152,18 +152,61 @@ text-align: right; } +.filter-subsection { + margin-bottom: var(--spacing-4); + + &:last-child { + margin-bottom: 0; + } +} + +.filter-subsection-label { + display: block; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-text-muted); + margin-bottom: var(--spacing-2); + padding-left: var(--spacing-1); +} + .sidebar .quick-filters { - list-style: square inside; + list-style: none; padding-left: 0; + margin: 0; - & a { + & li { + border-radius: var(--border-radius-sm); + overflow: hidden; + } + + & li + li { + margin-top: 1px; + } + + & a, & a.quick-filter-link { + display: block; + padding: var(--spacing-2) var(--spacing-3); color: var(--color-text-link); text-decoration: none; font-weight: var(--font-weight-medium); + font-size: var(--font-size-sm); + border-radius: var(--border-radius-sm); + transition: background var(--transition-fast), color var(--transition-fast); } & a:hover { - text-decoration: underline; + background: var(--color-bg-hover); + text-decoration: none; + } + + & a.is-active { + background: var(--color-primary-50); + color: var(--color-primary-700); + font-weight: var(--font-weight-semibold); + border-left: 3px solid var(--color-primary-500); + padding-left: calc(var(--spacing-3) - 3px); } } diff --git a/app/assets/stylesheets/components/topics.css b/app/assets/stylesheets/components/topics.css index c4de2b8..855ae8d 100644 --- a/app/assets/stylesheets/components/topics.css +++ b/app/assets/stylesheets/components/topics.css @@ -1,3 +1,64 @@ +.topics-filter-chip { + display: inline-flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-1) var(--spacing-3); + margin: var(--spacing-3) var(--spacing-4) 0; + background: var(--color-primary-50); + border: var(--border-width) solid var(--color-primary-200); + border-radius: 999px; + color: var(--color-primary-700); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + text-decoration: none; + transition: background var(--transition-fast), border-color var(--transition-fast); + + &:hover { + background: var(--color-primary-100); + border-color: var(--color-primary-500); + } +} + +.topics-filter-chip-close { + font-size: var(--font-size-xs); + opacity: 0.6; +} + +.topic-empty-row td { + border-bottom: none; + padding: var(--spacing-10) var(--spacing-4); +} + +.topics-empty-state { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-3); + color: var(--color-text-muted); + + & i { + font-size: 2rem; + opacity: 0.4; + } + + & p { + margin: 0; + font-size: var(--font-size-base); + } +} + +.topics-empty-clear { + color: var(--color-text-link); + font-weight: var(--font-weight-medium); + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + +/* ─────────────────────────────────────────────────────────────────── */ + .section-header { display: flex; justify-content: space-between; diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index f1ca8be..08bf1f1 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -234,4 +234,22 @@ def topic_title_link(topic) topic_path(topic) end end + + def topic_filter_labels + { + "no_contrib_replies" => "No contributor/committer replies", + "patch_no_replies" => "Patch, no replies", + "reading_incomplete" => "Reading in progress", + "new_for_me" => "New for me", + "started_by_me" => "Started by me", + "messaged_by_me" => "I posted here", + "starred_by_me" => "Starred by me", + "starred_by_team" => "Starred by team", + "team_unread" => "Not yet read by team", + "team_reading_others" => "Teammates reading", + "team_reading_any" => "Team reading", + "team_started" => "Started by team", + "team_messaged" => "Team messages" + } + end end diff --git a/app/views/topics/_sidebar.html.slim b/app/views/topics/_sidebar.html.slim index baadb96..81bf3a5 100644 --- a/app/views/topics/_sidebar.html.slim +++ b/app/views/topics/_sidebar.html.slim @@ -26,25 +26,39 @@ .sidebar-section h3.sidebar-heading Quick filters .sidebar-content - ul.quick-filters - li = link_to "No contributor/committer replies", topics_path(filter: "no_contrib_replies") - li = link_to "Patch, no replies", topics_path(filter: "patch_no_replies") - - if user_signed_in? - li = link_to "Reading in progress", topics_path(filter: "reading_incomplete") - li = link_to "New for me", topics_path(filter: "new_for_me") - li = link_to "Started by me", topics_path(filter: "started_by_me") - li = link_to "I posted here", topics_path(filter: "messaged_by_me") - li = link_to "Starred by me", topics_path(filter: "starred_by_me") - - current_user.teams.each do |team| - li = link_to "#{team.name}: starred by team", topics_path(filter: "starred_by_team", team_id: team.id) - li = link_to "#{team.name}: not yet read by team", topics_path(filter: "team_unread", team_id: team.id) - li = link_to "#{team.name}: my teammates are reading", topics_path(filter: "team_reading_others", team_id: team.id) - li = link_to "#{team.name}: my team is reading", topics_path(filter: "team_reading_any", team_id: team.id) - li = link_to "#{team.name}: started by team", topics_path(filter: "team_started", team_id: team.id) - li = link_to "#{team.name}: messages by team", topics_path(filter: "team_messaged", team_id: team.id) - - if user_signed_in? && @available_note_tags.present? - .sidebar-section - h3.sidebar-heading My tags - ul.quick-filters.tags-list - - @available_note_tags.each do |tag, count| - li = link_to "##{tag} (#{count})", topics_path(note_tag: tag) + - active_filter = params[:filter] + - active_team_id = params[:team_id]&.to_i + + .filter-subsection + span.filter-subsection-label Status + ul.quick-filters + li = link_to "No contributor/committer replies", topics_path(filter: "no_contrib_replies"), class: ("quick-filter-link is-active" if active_filter == "no_contrib_replies") + li = link_to "Patch, no replies", topics_path(filter: "patch_no_replies"), class: ("quick-filter-link is-active" if active_filter == "patch_no_replies") + + - if user_signed_in? + .filter-subsection + span.filter-subsection-label My activity + ul.quick-filters + li = link_to "Reading in progress", topics_path(filter: "reading_incomplete"), class: ("quick-filter-link is-active" if active_filter == "reading_incomplete") + li = link_to "New for me", topics_path(filter: "new_for_me"), class: ("quick-filter-link is-active" if active_filter == "new_for_me") + li = link_to "Started by me", topics_path(filter: "started_by_me"), class: ("quick-filter-link is-active" if active_filter == "started_by_me") + li = link_to "I posted here", topics_path(filter: "messaged_by_me"), class: ("quick-filter-link is-active" if active_filter == "messaged_by_me") + li = link_to "Starred by me", topics_path(filter: "starred_by_me"), class: ("quick-filter-link is-active" if active_filter == "starred_by_me") + + - current_user.teams.each do |team| + .filter-subsection + span.filter-subsection-label = team.name + ul.quick-filters + li = link_to "Starred by team", topics_path(filter: "starred_by_team", team_id: team.id), class: ("quick-filter-link is-active" if active_filter == "starred_by_team" && active_team_id == team.id) + li = link_to "Not yet read by team", topics_path(filter: "team_unread", team_id: team.id), class: ("quick-filter-link is-active" if active_filter == "team_unread" && active_team_id == team.id) + li = link_to "Teammates reading", topics_path(filter: "team_reading_others", team_id: team.id), class: ("quick-filter-link is-active" if active_filter == "team_reading_others" && active_team_id == team.id) + li = link_to "Team reading", topics_path(filter: "team_reading_any", team_id: team.id), class: ("quick-filter-link is-active" if active_filter == "team_reading_any" && active_team_id == team.id) + li = link_to "Started by team", topics_path(filter: "team_started", team_id: team.id), class: ("quick-filter-link is-active" if active_filter == "team_started" && active_team_id == team.id) + li = link_to "Team messages", topics_path(filter: "team_messaged", team_id: team.id), class: ("quick-filter-link is-active" if active_filter == "team_messaged" && active_team_id == team.id) + + - if user_signed_in? && @available_note_tags.present? + .sidebar-section + h3.sidebar-heading My tags + ul.quick-filters.tags-list + - @available_note_tags.each do |tag, count| + li = link_to "##{tag} (#{count})", topics_path(note_tag: tag), class: ("quick-filter-link is-active" if params[:note_tag] == tag) diff --git a/app/views/topics/_topics.html.slim b/app/views/topics/_topics.html.slim index 431a2d6..0fb3a5f 100644 --- a/app/views/topics/_topics.html.slim +++ b/app/views/topics/_topics.html.slim @@ -1,5 +1,14 @@ - topic_participants_map ||= @topic_participants_map || {} +- if topics.empty? + tr.topic-empty-row + td colspan=3 + .topics-empty-state + i.fa-regular.fa-folder-open aria-hidden="true" + p No topics match this filter. + - if params[:filter].present? || params[:note_tag].present? + = link_to "Clear filters", topics_path, class: "topics-empty-clear" + - topics.each do |topic| - tp_data = topic_participants_map[topic.id] || {} - top_participants = tp_data[:top] || [] diff --git a/app/views/topics/index.html.slim b/app/views/topics/index.html.slim index aa4ba39..e7d91bf 100644 --- a/app/views/topics/index.html.slim +++ b/app/views/topics/index.html.slim @@ -1,12 +1,18 @@ - cache_block = lambda do + - if params[:filter].present? + = link_to topics_path(note_tag: params[:note_tag]), class: "topics-filter-chip" do + i.fa-solid.fa-filter aria-hidden="true" + span = topic_filter_labels[params[:filter]] || params[:filter].humanize + i.fa-solid.fa-xmark.topics-filter-chip-close aria-hidden="true" + - elsif params[:note_tag].present? + = link_to topics_path, class: "topics-filter-chip" do + i.fa-solid.fa-tag aria-hidden="true" + span = "##{params[:note_tag]}" + i.fa-solid.fa-xmark.topics-filter-chip-close aria-hidden="true" + #new-topics-banner data-controller="new-topics-banner" data-new-topics-banner-url-value=new_topics_count_topics_path(viewing_since: @viewing_since.iso8601, filter: params[:filter], team_id: params[:team_id]) data-new-topics-banner-interval-ms-value="180000" = render partial: "new_topics_banner", locals: { count: @new_topics_count, viewing_since: @viewing_since } - - if @active_note_tag.present? - .tag-filter-banner - span Filtering by ##{@active_note_tag} - = link_to "Clear", topics_path, class: "clear-tag-filter" - .topics-table table thead