#Search
One header, one grammar. Every list screen — People, Tags, Questions,
All to-dos, both doors and every group screen — renders ToolHeader: the
screen header with two icons in its right slot, Search and Filter, each
closed until asked for, so the resting screen is only its list. Search
slides a bar in under the header, focused, with Cancel; it stays open
whenever the query is non-empty, so a result's Back lands on the same
search, and Cancel clears and closes. The box itself is SearchField
(components/SearchField.tsx, the merge card's box too): a small ✕ inside
the field at its right edge, there only while there is text, which empties
the box and keeps the focus — and, in the header, keeps the bar open —
where Cancel closes it; the browser's own clear on type="search" is
hidden so there is one ✕ everywhere. The box holds its own draft, because
the query lives in the URL and comes back a render late — a controlled
input reset to the stale prop between two fast keystrokes lost the first
— and reads the prop as an acknowledgement: a value the field sent is
settled, one it never sent (Cancel, Escape, Back onto another query)
replaces the draft, and values in flight wait in order so an echo that has
fallen behind the typing is never read as a change. The one exception is the People
directory (search.always): its box is the resting screen — no icon, no
Cancel, never focused on arrival, Escape clearing — because a directory is
opened to look somebody up. Filter opens FilterSheet, a bottom
sheet holding the group tree (All, then every group two levels deep with a
caret to fold a parent's children, groupTree) and the sort, whichever the
screen has; a row applies on the tap and the sheet closes. An applied group
is one summary chip under the header, its path spelled out ("Renovation ›
Kitchen ✕"), and a dot on the icon; the sort shows nowhere but the sheet.
A Who section — Everyone, or Mine — sits between the two on All
to-dos, the calendar and Questions, and only once the signed-in account
has claimed a person (personForUid; the row wears that person's name in
the nickname's voice): "Mine" is what NAMES that person, through the
mention scanner (mentionsPerson; a question also by askOf,
questionFor), wherever it is filed, and it rides the query string as
?who=me beside ?g=, each write preserving the other, with its own
"Mine ✕" chip under the header. A link carrying who=me onto a phone
whose account has claimed nobody is inert — everything shows — rather than
showing nothing.
Which screen has which: the directories and Questions filter by group;
All to-dos and All notes by group and order; a notes group by order; to-do
groups and the doors search only; the calendar filters by group, its title bar back
for the purpose; All to-dos, the calendar and Questions also by who. See DECISIONS, "Search and filter live behind the
header".
The grammar (src/lib/search/search.ts, parseQuery /
matchesSearch): a query is words, tags and people, told apart by how they
are typed. #boiler matches a document's tags by prefix, never the word in
a sentence; @sam matches a document that mentions someone answering to
that handle, through the fold the mentions resolve through (an unresolved
mention answers by its typed handle); anything else is words, every token
somewhere in the folded text. All of them narrow together — AND, no OR, no
quoting. While a # or @ token is being typed at the end of the query,
the tags in view (tagChoices) or the household (mentionChoices) are
offered as chips under the bar, and a tap finishes the token
(completionAt / completeQuery). The People and Tags directories search
their own names with plain words and offer no completions.
On a door or a group screen the search looks at that level and everything
filed under it, and never up. What it finds is documents and folders
both. From a group's
screen the rows become every active note (or open to-do) in the subtree —
the group's own and its subgroups' — that matches, each brought up from a
subgroup saying which (NoteRow's where, TaskRow's showGroup), and
the cards become the subgroups whose names match (searchGroupCards); the
archived shelf and the # completions read off the same widened set. From a
door the level is everything on that surface: every group at the top and
one down, and every document wherever it is filed — rendered by
DocumentHits as ways in, not as content rows, because the door never
renders content: a note opens on its page with Back leading to the door,
search and all; a to-do lands on its group with the row flashing. A
subgroup's screen searches only itself. The query is q in the query
string, written with replace like every filter in the app. See DECISIONS,
"Search looks down, never up".