1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
require "pro_tacts/admin/phlex"

require "pro_tacts/admin/group_label"
require "pro_tacts/admin/list_item"
require "pro_tacts/admin/recent_row"

module ProTacts
  module Admin
    # The list under the search's input (SearchField), which
    # GET /search/results answers alone and the search page renders
    # in: the contacts a query matches in the order Search ranks them
    # and then the groups — a group is a record the search reaches as
    # surely as a contact is (docs/DESIGN.md, "When adding something
    # new"). The contacts stop at LIMIT, and the rest are counted
    # rather than listed: the answer to a query that long is a longer
    # query (docs/plans/2026-10-17-search-keys.md).
    class SearchResults < Phlex::HTML
      LIMIT = 50 #: Integer

      # @rbs @contacts: Array[Store::RecentContact]
      # @rbs @groups: Array[Group]
      # @rbs @recent: bool
      # @rbs @more: Integer

      # `recent` is the list standing in for results before there is a
      # query — the most recently updated contacts, labeled as the
      # dashboard labels them (Web#search_results).
      # `more` is how many matched past LIMIT.
      #: (contacts: Array[Store::RecentContact], groups: Array[Group], ?recent: bool, ?more: Integer) -> void
      def initialize(contacts:, groups:, recent: false, more: 0)
        @contacts = contacts
        @groups = groups
        @recent = recent
        @more = more
      end

      def view_template
        if @contacts.empty? && @groups.empty?
          p(class: "type-body-sm") { @recent ? "No contacts yet." : "Nothing matches." }
          return
        end

        if @contacts.any?
          h2(class: "type-label") { @recent ? "recently updated" : "contacts" }
          ul(class: "card") { @contacts.each { render RecentRow.new(row: it) } }
          p(class: "type-body-sm") { "#{@more} more. Keep typing to narrow them down." } if @more.positive?
        end
        return if @groups.empty?

        h2(class: "type-label") { "groups" }
        ul(class: "card") do
          @groups.each do |group|
            render ListItem.new(href: "/groups/#{group.id}") { render GroupLabel.new(group:) }
          end
        end
      end
    end
  end
end