aboutsummaryrefslogtreecommitdiff
path: root/docs/design/2026-05-28-pattern-catalog-no-empty-input.org
blob: d2b19c8a3cd93ce23ce885f015442555b3c4bced (plain)
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
#+TITLE: Pattern catalog — "no empty input as meaningful" (third worked example)
#+DATE: [2026-05-28 Thu]
#+SOURCE: pearl issue-sources verify, follow-up to earlier UI-patterns handoff

* Follow-up note

A third worked example landed in pearl on 2026-05-28, shipped as commit =1288c2a=. Adding it here so the rulesets catalog discussion has three concrete examples to ground design questions in.

* Pattern: "no empty input as meaningful"

Whenever a prompt offers the user a choice between picking a value and picking "no value," the no-value option belongs in the candidate list, not behind empty input.

** Before

The pearl ad-hoc filter builder had five sequential =completing-read= prompts (team, state, project, labels, assignee). Four of them relied on the "empty input means no constraint" convention, with a parenthetical hint in the label:

#+begin_example
Team (empty for any): _
State (empty for any): _
Project (empty for any): _
Labels (comma-separated, empty for none): _
#+end_example

The user had to remember the convention. RET on an unselected prompt did something silently. The label hint was the only on-screen affordance, and it lived in the prompt rather than the candidate list, where the eye was actually looking.

** After

#+begin_example
Team: [ None. ]   <-- first candidate, picked by default RET
                  Engineering
                  Design
                  Marketing
                  ...
#+end_example

Same logical behavior. The "no constraint" choice is now a candidate the user sees and picks, not a convention they have to recall. =require-match= is on, so the user picks from the list rather than free-typing.

** What changed in code

Two helpers (one predicate, one list-prepender) and a defconst hold the sentinel string. Each =completing-read= wraps its candidate list with the prepender and treats the sentinel as the same logical opt-out empty input was before. Trivial back-compat: the predicate also returns true for nil and empty string, so any caller that still produces an empty answer (e.g. =completing-read-multiple= returning nothing) is handled.

** Why it matters for the catalog

The principle is small but has a lot of surface area: every prompt across every project where "none" is a meaningful choice is a candidate. The cost is one helper per project (or one shared helper); the win is users stop having to know rules that aren't on screen.

Worth thinking about as a catalog entry alongside the earlier two examples (one-prompt picker with typed prefix, magit-transient state-buttons). All three share the same underlying principle: *no hidden affordances; if it's a choice, it's in the list.*

* What the catalog might need to capture for each pattern

Surfacing this third example clarifies what a catalog entry probably needs to carry. Tentative shape:

- One-sentence statement of the principle
- The before / after the pattern replaced
- The shape of the code change (small helpers, single source of truth)
- Where the pattern applies (the surface area predicate)
- Anti-pattern variants to avoid
- A link or grep target for an in-the-wild example

If that shape stabilizes, the catalog has a chance of staying scannable as it grows.