contacts: drop the filter for the header search
The filter matched a name; the search reads every field,
so the weaker match is gone, and to the search the list will yield.
The pickers keep the filter where a box is ticked to pick;
this page is read by letter, and the search is one key quick.
Assisted-by: Claude Opus 5.5 via Claude Code
diff --git a/lib/pro_tacts/admin/contacts_index.rb b/lib/pro_tacts/admin/contacts_index.rb
index ae96dab..e937a37 100644
--- a/lib/pro_tacts/admin/contacts_index.rb
+++ b/lib/pro_tacts/admin/contacts_index.rb
@@ -2,7 +2,6 @@ require "pro_tacts/admin/phlex"
require "pro_tacts/admin/avatar"
require "pro_tacts/admin/format"
-require "pro_tacts/admin/group_filter"
require "pro_tacts/admin/group_label"
require "pro_tacts/admin/layout"
@@ -14,11 +13,10 @@ module ProTacts
# label the dashboard's rows do (Format.name_label) and the
# contact's groups as chips under it.
#
- # A filter over the rows narrows the list by that label as it is
- # typed, GroupFilter's the way the members screen's is, so the
- # book's two lists of contacts match a name the same way. A letter
- # whose rows it all hides goes with them, heading and card, rather
- # than standing as an empty section.
+ # Nothing on the page narrows the list. Finding a contact is the
+ # header's search, which matches every field rather than the name
+ # alone; a filter here was a second, weaker match that could only
+ # disagree with it. This page is for reading the book by letter.
class ContactsIndex < Phlex::HTML
# @rbs @rows: Array[Contact]
# @rbs @login: String
@@ -39,17 +37,16 @@ module ProTacts
def view_template
render Layout.new(title: "Contacts", login: @login) do
- section(class: "contacts-list", x_data: GroupFilter::STATE) do
+ section(class: "contacts-list") do
div(class: "section-head") do
h2(class: "type-label") { "contacts (#{@rows.length})" }
end
if @rows.empty?
p(class: "type-body-sm") { "No contacts yet." }
else
- render GroupFilter.new(placeholder: "Filter contacts")
- div(class: "letter-groups", x_ref: "options") do
+ div(class: "letter-groups") do
letter_groups.each do |letter, rows|
- section(":hidden": "![...$el.querySelectorAll('[data-label]')].some(row => visible(row))") do
+ section do
h3(class: "type-label") { letter }
ul(class: "card") do
rows.each { render_row(it) }
@@ -57,7 +54,6 @@ module ProTacts
end
end
end
- render GroupFilter::Empty.new(any: true, offer: false, noun: "contacts")
end
end
end
@@ -72,7 +68,7 @@ module ProTacts
# name is the row's link and stretches over the row
# (.linked-row in admin.css); the chips, each a link to its
# group's page as on the record page, sit above that stretch.
- li(class: "linked-row", **GroupFilter.row(Format.name_label(contact))) do
+ li(class: "linked-row") do
render Avatar.new(contact:, size: "lg")
div(style: "flex: 1; min-width: 0;") do
a(href: "/contacts/#{contact.id}", class: "row-link") { Format.name_label(contact) }
diff --git a/lib/pro_tacts/admin/group_filter.rb b/lib/pro_tacts/admin/group_filter.rb
index af6e0ff..aea0c1a 100644
--- a/lib/pro_tacts/admin/group_filter.rb
+++ b/lib/pro_tacts/admin/group_filter.rb
@@ -10,12 +10,10 @@ module ProTacts
# it, and the offer to make the group the filter names
# (docs/plans/2026-09-22-a-few-groups-at-a-time.md).
#
- # Shared by the three places too long a list is picked from, and
- # by the one it is only read from, the contacts page
- # (Admin::ContactsIndex). Of the three pickers, the two where a
- # contact's groups are picked — the dialog on a stored contact's
- # card (Admin::GroupDialog) and the boxes beside an arriving one
- # (Admin::ImportGroups) — and the members screen,
+ # Shared by the three places too long a list is picked from: the
+ # two where a contact's groups are picked — the dialog on a stored
+ # contact's card (Admin::GroupDialog) and the boxes beside an
+ # arriving one (Admin::ImportGroups) — and the members screen,
# where a group's contacts are (Admin::GroupsMembers). A book with
# three groups and a book with three hundred are the same screen,
# and the import's is the screen the walk opens once per contact,
@@ -25,9 +23,11 @@ module ProTacts
# and not the offer to make what the filter names: a filter word
# cannot mint a contact — that is the dashboard's dialog, with
# its name fields — so its placeholder says nothing about adding,
- # and `named` idles, nothing rendering Fresh to push onto it. The
- # contacts page renders the filter alone: its rows have no boxes,
- # and nothing caps a list whose whole point is the whole book.
+ # and `named` idles, nothing rendering Fresh to push onto it.
+ #
+ # The contacts page does not use it: a list that is only read, not
+ # picked from, leaves finding to the header's search, which matches
+ # more than a name (Admin::ContactsIndex).
#
# Alpine does the filtering, matching each row's label as
# Format.search_key folds it — lowercased, accents folded —
@@ -57,10 +57,9 @@ module ProTacts
# make one vanish would be hiding a decision rather than
# narrowing a list — and the tick is read off the checkbox at
# each evaluation, so a row answers the next filter change
- # already ticked, and a row with no box at all — the contacts
- # page's — is ticked by nobody. `rows` is whatever carries
- # `data-label` — a label in the dialog and the import, an li on
- # the members screen and the contacts page, whose label sits inside its row — and is the element
+ # already ticked. `rows` is whatever carries `data-label` — a
+ # label in the dialog and the import, an li on the members
+ # screen, whose label sits inside its row — and is the element
# list `none` reads: no row visible, which is what "No groups
# match" claims. `named` is the names a tick on the offer
# committed to making (Named): `creatable` and `none` read it
diff --git a/public/admin.css b/public/admin.css
index fb7383f..30a518e 100644
--- a/public/admin.css
+++ b/public/admin.css
@@ -943,14 +943,6 @@ section > h2 {
line-height: calc(var(--gl-step--1) * 1.55);
}
-/* The contacts page's filter (Admin::ContactsIndex), between the
- heading and the letters it narrows: the section-head's own step to
- the list, so the field reads as the list's first line rather than
- as the heading's. */
-.contacts-list > .filter {
- margin-bottom: var(--gl-space-s);
-}
-
/* A row with links inside it (ContactsIndex's rows, whose group
chips open their groups): Gloss's whole-row control is an anchor
that is the row's only child, and an anchor cannot hold the chips'
diff --git a/test/pro_tacts/admin/test_contacts_pages.rb b/test/pro_tacts/admin/test_contacts_pages.rb
index e74e8ce..0f417e8 100644
--- a/test/pro_tacts/admin/test_contacts_pages.rb
+++ b/test/pro_tacts/admin/test_contacts_pages.rb
@@ -205,21 +205,13 @@ class AdminContactsPagesTest < Minitest::Test
end
end
- # The filter is the members screen's (Admin::GroupFilter): each row
- # carries the label it matches on, lowercased, and each letter hides
- # once the filter hides every row under it.
- def test_contacts_filters_the_rows_by_name
- red = ADA.sub("FN:Ada Lovelace", "FN:Sarah\r\nNICKNAME:Red").sub("UID:ada", "UID:red")
-
- with_contacts({"red" => red}) do
- get "/contacts"
+ # Finding a contact is the header's search, which matches more than
+ # a name; the list has no filter of its own to disagree with it.
+ def test_contacts_has_no_filter_of_its_own
+ with_contacts({"ada" => ADA}) { get "/contacts" }
- body = last_response.body
- assert_includes body, %(placeholder="Filter contacts")
- assert_includes body, %(data-label="sarah (red)")
- assert_includes body, %(<section :hidden="![...$el.querySelectorAll('[data-label]')].some(row => visible(row))">)
- assert_includes body, "No contacts match."
- end
+ refute_includes last_response.body, "Filter contacts"
+ refute_includes last_response.body, "data-label="
end
def test_contacts_with_no_contacts_says_so
@@ -227,7 +219,6 @@ class AdminContactsPagesTest < Minitest::Test
assert_includes last_response.body, "contacts (0)"
assert_includes last_response.body, "No contacts yet."
- refute_includes last_response.body, "Filter contacts"
end
# The link's answer: every stored card's bytes in id order — the