admin: render the header search on every page
Assisted-by: GLM-5.3 via pi
diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index 8618fac..51eed25 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -62,7 +62,11 @@ thousand, so no screen exists to scroll the whole set.
Every collection screen is the same shape:
-1. A search input in the page header, focused and ready.
+1. A search input in the page header, focused and ready. The input
+ renders on every screen — a header that changes height between
+ pages makes the chrome jump, and a search should never require
+ going home first — but focuses only on collection screens, where
+ the search is the page.
2. Results while there's a query.
3. **Recently updated** when there isn't — the ten or so records touched last,
sorted by update time.
diff --git a/lib/pro_tacts/admin/contacts_index.rb b/lib/pro_tacts/admin/contacts_index.rb
index bdfc002..7bad30a 100644
--- a/lib/pro_tacts/admin/contacts_index.rb
+++ b/lib/pro_tacts/admin/contacts_index.rb
@@ -38,7 +38,8 @@ module ProTacts
end
def view_template
- render Layout.new(title: "Contacts", wide: true, search: @query, notice: @notice) do
+ render Layout.new(title: "Contacts", wide: true, query: @query,
+ autofocus: @query.empty?, notice: @notice) do
div(class: "dashboard") do
section do
div(class: "section-head") do
diff --git a/lib/pro_tacts/admin/layout.rb b/lib/pro_tacts/admin/layout.rb
index 546e123..f5be6ee 100644
--- a/lib/pro_tacts/admin/layout.rb
+++ b/lib/pro_tacts/admin/layout.rb
@@ -6,18 +6,24 @@ module ProTacts
# vendored Gloss stylesheets (see public/vendor/gloss and
# docs/DESIGN.md), and a one-line header naming the app in type —
# "no brand mark" is one of the rules that document inherits from
- # Gloss. A screen that passes `search` renders the collection
- # search in that header, beside the name (docs/DESIGN.md's
- # search-first rule). `wide` opts a screen out of the reading
- # width a single column wants (see admin.css) — the dashboard
- # root is the one screen that asks. No JavaScript: nothing served
- # here yet needs any.
+ # Gloss — with the collection search beside the name on every
+ # screen: a header that grew and shrank with the search made the
+ # chrome jump between pages, and search-first (docs/DESIGN.md)
+ # wants finding a contact possible from anywhere. `query` carries
+ # the dashboard's current search into the input's value;
+ # `autofocus` is a screen saying its entry point is the search —
+ # the dashboard with an empty query, the one place the design
+ # doc wants "focused and ready". `wide` opts a screen out of the
+ # reading width a single column wants (see admin.css) — the
+ # dashboard root is the one screen that asks. No JavaScript:
+ # nothing served here yet needs any.
class Layout < Phlex::HTML
- #: (title: String, ?wide: bool, ?search: String?, ?notice: String?) -> void
- def initialize(title:, wide: false, search: nil, notice: nil)
+ #: (title: String, ?wide: bool, ?query: String?, ?autofocus: bool, ?notice: String?) -> void
+ def initialize(title:, wide: false, query: nil, autofocus: false, notice: nil)
@title = title
@wide = wide
- @search = search
+ @query = query
+ @autofocus = autofocus
@notice = notice
end
@@ -38,15 +44,13 @@ module ProTacts
body do
header(class: "admin-header") do
a(href: "/") { "pro-tacts" }
- # The dashboard's query follows the search into the
- # header: the input keeps its value across a search, and
- # focuses only when the query is empty — a results page
- # has somewhere to be besides the input.
- if @search
- form(action: "/", method: "get", class: "search-form") do
- input(type: "search", name: "q", value: @search,
- placeholder: "Search contacts", autofocus: @search.to_s.empty?)
- end
+ # Every screen's chrome, not the dashboard's alone: the
+ # input keeps the query it carries — a results page has
+ # somewhere to be besides the input — and focuses only
+ # where the screen asked for it.
+ form(action: "/", method: "get", class: "search-form") do
+ input(type: "search", name: "q", value: @query,
+ placeholder: "Search contacts", autofocus: @autofocus)
end
end
main(class: "admin-main", **(@wide ? {data: {wide: true}} : {})) { yield }
diff --git a/test/pro_tacts/admin/test_contacts_pages.rb b/test/pro_tacts/admin/test_contacts_pages.rb
index 198b69f..7876d61 100644
--- a/test/pro_tacts/admin/test_contacts_pages.rb
+++ b/test/pro_tacts/admin/test_contacts_pages.rb
@@ -132,9 +132,11 @@ class AdminContactsPagesTest < Minitest::Test
end
# The search lives in the page header (docs/DESIGN.md), rendered by
- # the layout when a screen asks for it. A screen that forgets to ask
- # loses the input with no route failing — search still works through
- # the URL — so this pins the wiring, not just the behavior.
+ # the layout on every screen now — not just the dashboard's — so
+ # the header's height never changes between pages and finding a
+ # contact never requires going home first. This pins the wiring,
+ # not just the behavior: a screen that forgets `autofocus` loses
+ # the focused-and-ready entry point with no route failing.
def test_the_search_lives_in_the_header
with_contacts({"ada" => ADA}) do
get "/"
@@ -150,6 +152,23 @@ class AdminContactsPagesTest < Minitest::Test
end
end
+ # The same chrome on a record's own page: the search rides the
+ # header there too, empty and unfocused — the record is that
+ # page's content, not a query.
+ def test_the_search_renders_on_detail_pages_too
+ with_contacts({"ada" => ADA}) do
+ get "/contacts/ada"
+
+ header = last_response.body.split("<main").first
+ assert_includes header, "search-form"
+ # No value attribute at all — the record's page carries no
+ # query, and Phlex omits the attribute for nil rather than
+ # rendering it empty.
+ assert_includes header, '<input type="search" name="q" placeholder="Search contacts">'
+ refute_includes header, "autofocus"
+ end
+ end
+
# Searching narrows the contacts column; the ambient column stays
# where it is, still answering its own question.
def test_search_leaves_the_birthdays_column_standing