Centre the command palette, and write down how the two design commitments fit

0a00a0408c61 · AtlantisPleb · · parent 05066139ec73

Centre the command palette, and write down how the two design commitments fit

A modal `<dialog>` is centred by the user agent's `margin: auto`, and the
reset zeroes margin on every element, so setting only `margin-block-start`
left the inline margins at 0 and the palette opened hard against the left
edge. Both axes are stated now. This is the third time that trap has appeared
-- `.menu` hit it with `margin: 0` -- so the comment names it.

The doc records the principle and, more usefully, where it actually bites.
GitHub owns the contract: paths, resource names, JSON fields, status codes.
Linear owns the reading: density, what a row shows at rest, how status is
signalled, what a keyboard reaches. Almost every decision falls cleanly on one
side of that line.

Three collide, and the GitHub spec turns out to answer two of them itself. The
current issue schema carries `issue_field_values` -- typed custom fields with
`single_select` among the data types -- which is exactly how six status
categories and four priority levels live inside an API whose `state` is
binary. `state` derives from Status rather than the other way round, so a
client that asked for a GitHub issue gets one. Grouping and cycles are the
third: grouping is a view concern and needs no contract change, and cycles are
not milestones and should not pretend to be.

The spec also gives us three things Circle has no vocabulary for -- issue
types, sub-issue summaries, and dependency summaries -- so parity is not
purely a tax.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149rBWy7br1Z7bbz9NrQhEr
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified assets/css/openagents.css
  • added docs/2026-08-20-linear-design-github-shape.md

Diff

2 files changed, +161 -1

assets/css/openagents.css modified +7 -1

@@ -5850,7 +5850,13 @@

5850 5850
  .command-palette {
5851 5851
    width: min(640px, calc(100vw - 32px));
5852 5852
    max-width: none;
5853
    margin-block-start: 12vh;
5853
    /* A modal dialog is centred by the user agent's `margin: auto`, and the
5854
       reset zeroes margin on every element -- so setting only the block-start
5855
       left the inline margins at 0 and the palette opened against the left
5856
       edge. Both axes are stated: `auto` inline to centre, 12vh above so it
5857
       sits under the eye's resting point rather than in the middle. This is
5858
       the same trap `.menu` hit with `margin: 0`. */
5859
    margin: 12vh auto auto;
5854 5860
    padding: 0;
5855 5861
    border: 1px solid var(--line-strong);
5856 5862
    border-radius: var(--radius-xl);
docs/2026-08-20-linear-design-github-shape.md added +154

@@ -0,0 +1,154 @@

1
# Linear's design language over GitHub's shape
2
3
*2026-08-20*
4
5
The principle, stated by the owner:
6
7
> Use the Linear/Circle design language as much as possible while keeping the
8
> UI/API shape at parity with GitHub, at least for now.
9
10
This doc says how those two survive each other. It is a strategy note, not a
11
plan of record: the tracker for the component work is
12
`docs/2026-08-20-circle-ui-port.md`, and the URL/API compatibility target is
13
`~/work/sarah/docs/audits/2026-08-19-github-clone-harvest-candidates.md`.
14
15
## The two commitments are about different layers
16
17
They only look like they conflict.
18
19
**GitHub owns the contract.** Paths, resource names, JSON field names, status
20
codes. `/:owner/:repo/issues/:number`, `GET /api/v3/repos/:owner/:repo/issues`,
21
`state: "open" | "closed"`, `state_reason`, `labels[]`, `assignees[]`,
22
`milestone`. An existing client, a bookmark, a git remote, or a `gh`-shaped
23
tool should keep working after a hostname swap. That is a compatibility
24
promise, and compatibility promises are kept in the wire format.
25
26
**Linear owns the reading.** Density, type scale, what a row shows at rest and
27
on hover, how status is signalled, what a keyboard can reach, how much chrome
28
sits between a person and the work. None of that appears in a JSON payload.
29
30
So: **GitHub decides what a thing is called and where it lives. Linear decides
31
what it looks like and how it is operated.** Almost every design decision falls
32
cleanly on one side.
33
34
## Where they actually collide
35
36
Three places, and only three. Everything else is a false conflict.
37
38
### 1. Status: two categories versus six
39
40
GitHub's issue has `state`, and it is binary — `open` or `closed` — with
41
`state_reason` narrowing a close (`completed`, `not_planned`, `duplicate`,
42
`reopened`). Circle's issue has six: backlog, todo, in progress, in review,
43
done, cancelled.
44
45
The temptation is to add a `status` column. That breaks parity the moment a
46
client reads it, because the client asked for a GitHub issue.
47
48
**GitHub has already solved this, and recently.** The current issue schema
49
carries `issue_field_values`, an array of typed custom fields:
50
51
```
52
issue-field-value: issue_field_id, issue_field_name, data_type, value,
53
                   single_select_option, multi_select_options
54
data_type: text | single_select | multi_select | number | date
55
```
56
57
A `single_select` field named Status, whose options are the six categories, is
58
**exactly** how GitHub models this today. It is not a workaround. We already
59
have `project_fields` with `name`, `data_type` and `options`, which is the same
60
shape one level up.
61
62
The mapping that keeps both promises:
63
64
| Reading | Writing |
65
| --- | --- |
66
| `state` stays derived: anything but done/cancelled is `open` | a client writing `state: "closed"` sets the Status field to done |
67
| `state_reason` derives too: cancelled → `not_planned` | a client writing `state_reason: "not_planned"` sets cancelled |
68
| the Status field is the richer truth | the UI writes the field; `state` follows |
69
70
A GitHub client sees an issue that opens and closes. A person sees six
71
categories. Neither is being lied to, because `open` genuinely is "not done and
72
not cancelled".
73
74
### 2. Priority: GitHub has no such field
75
76
Circle has four levels. GitHub has none — priority is conventionally a label
77
(`priority: high`), which is why every GitHub-shaped tool grew a label
78
convention.
79
80
Same answer, and it is the better one: a `single_select` custom field named
81
Priority. Labels stay labels — a flat, user-defined vocabulary — instead of
82
being overloaded into an ordered scale they cannot express. `issue_priority/1`
83
already renders four levels from one shape, so the UI is waiting on the field,
84
not the other way round.
85
86
If a client expects the label convention, emit **both**: the field is the
87
truth, a `priority: high` label is a projection for tools that only read
88
labels. Projections are cheap; a second source of truth is not.
89
90
### 3. Grouping and cycles
91
92
Circle groups by status, assignee, priority, or project, and has cycles.
93
GitHub's list endpoint filters (`state`, `labels`, `assignee`, `milestone`,
94
`since`) but does not group, and has milestones rather than cycles.
95
96
Grouping is a **view** concern: the list endpoint returns issues, the page
97
groups them. Nothing to reconcile — do not add a `group_by` parameter to a
98
GitHub-shaped endpoint.
99
100
Cycles are not milestones and should not pretend to be. A milestone has a due
101
date and a fixed scope; a cycle is a repeating window that rolls unfinished
102
work forward. Until there is a reason to model cycles, milestones are the
103
GitHub-shaped answer and the UI should say "milestone".
104
105
## What GitHub gives us that Linear's model does not
106
107
Worth noting, because the parity constraint is not purely a tax. The current
108
schema also carries:
109
110
- **`type`** — an issue type (bug, feature, task) with a colour, enabled per
111
  org. Circle's issue has no equivalent.
112
- **`sub_issues_summary`** — `total`, `completed`, `percent_completed`. Native
113
  parent/child issues.
114
- **`issue_dependencies_summary`** — blocking relationships.
115
116
Circle's UI has no vocabulary for these, so they are ours to design rather than
117
port. The progress arc from `issue_status/1` is the obvious renderer for
118
`sub_issues_summary.percent_completed` — it already fills from a number.
119
120
## Rules of thumb
121
122
1. **A design decision that changes a payload is not a design decision.** If
123
   adopting a Linear pattern would add or rename a JSON field, stop: it belongs
124
   in a custom field, a projection, or the view layer.
125
2. **Custom fields before columns.** Anything Linear has and GitHub does not is
126
   a `single_select` field until proven otherwise. That is GitHub's own answer.
127
3. **Derive the compatibility surface, never the rich one.** `state` is
128
   computed from Status. Never the reverse, or the two drift and the API starts
129
   lying.
130
4. **Grouping, sorting, density, and keyboard access are free.** They touch no
131
   contract. Take all of them.
132
5. **Do not rename GitHub's nouns in the UI.** A milestone is called a
133
   milestone even if a cycle would be nicer, because the URL and the API say
134
   milestone and a person who follows one to the other should not have to
135
   translate.
136
6. **Tokens, not a second palette.** The same rule the component ports follow:
137
   Circle's design language arrives as composition and density, never as its
138
   colour values.
139
140
## Where this leaves the current work
141
142
The Circle components exist and are catalogued; nothing renders them on a real
143
page yet, because doing so needs the Status and Priority fields above. That is
144
the next decision, and it is a schema decision rather than a UI one — which is
145
why the port stopped at the component library rather than guessing at it.
146
147
Order that follows from this doc:
148
149
1. Custom fields on issues (`issue_field_values`-shaped), with Status and
150
   Priority as the first two.
151
2. Derive `state` and `state_reason` from Status, both directions, with tests
152
   that a GitHub-shaped client sees no change.
153
3. Then wire `issue_row/1`, `issue_group/1` and `issue_toolbar/1` into the
154
   issue index, which by then is only a view change.

This page updates live while a promote is in flight · changelog