Planet4.Me docs
Open the app

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".