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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
require "pro_tacts/admin/phlex"

require "pro_tacts/admin/format"
require "pro_tacts/admin/icons"

module ProTacts
  module Admin
    # The contacts an uploaded .vcf holds, down the side of every
    # screen of the import. Why a sidebar, and the two facts each row
    # carries — the verdict at its trailing edge, the count of lines
    # the card is losing under its name — are
    # docs/plans/2026-09-21-import-a-vcf.md, "The list is a sidebar".
    # The verdict's word stays for a screen reader
    # (.gl-visually-hidden); the count wears a change log entry's mark
    # (Admin::ContactsShow#diff_lines, .diff-removed in admin.css).
    class ImportSidebar < Phlex::HTML
      # @rbs @upload: String
      # @rbs @rows: Array[[String, Integer, Integer]]
      # @rbs @saved: Hash[String, String]
      # @rbs @current: Integer?

      # `rows` is the contact each card will be written as, how many
      # of its original's lines are not coming with it, and the card's
      # place in the file, in the order the list reads
      # (Web#walk_order); `saved` the contact each row that has come in
      # was written as, by the row's place in the file; `current` the
      # row whose screen is open.
      #: (upload: String, rows: Array[[String, Integer, Integer]], saved: Hash[String, String], current: Integer) -> void
      def initialize(upload:, rows:, saved:, current:)
        @upload = upload
        @rows = rows
        @saved = saved
        @current = current
      end

      # The record's own column shape (.record in admin.css), with no
      # caption row (the plan, "The list is a sidebar").
      def view_template
        nav(class: "record walk-list") do
          ul(class: "card") do
            @rows.each do |(label, dropped, index)|
              row(label, dropped, index)
            end
          end
        end
      end

      private

      #: (String label, Integer dropped, Integer index) -> void
      def row(label, dropped, index)
        stored = @saved[index.to_s]
        li do
          # Every row opens in the walk, a saved one included, as the
          # contact it became beside the card it arrived as
          # (Web#card_screen; the plan, "Nothing waits for the end").
          # `aria-current` marks the row being read, the one thing a
          # list beside its own content has to say that a list on a
          # page of its own does not.
          a(href: "/import/#{@upload}/#{index}",
            aria_current: ("page" if index == @current),
            data: {state: stored ? "saved" : "unsaved"}) do
            div(style: "flex: 1; min-width: 0;") do
              div(style: "font-weight: 550;") { label }
              unless stored
                span(class: "type-label diff-removed") { "-#{dropped}" } if dropped.positive?
              end
            end
            span(class: "gl-visually-hidden") { stored ? "saved" : "not saved" }
            render Icon.new(stored ? :circle_check : :circle)
          end
        end
      end
    end
  end
end