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