steid

@jamesgill /

steid/plans/ui.md
16.9 KBCode·Blame·Raw
1# UI
2
3## Philosophy
4
5A developer tool people use daily to browse code and manage repos. Optimise for:
6
71. **Speed-first** — minimise clicks, maximise information density
82. **Scannable** — find what you need in under a second
93. **Quiet confidence** — premium without flashy; the UI should disappear
104. **Code-centric** — code is the hero, everything else supports it
11
12| Principle | Meaning |
13|---|---|
14| Readable density | Compact without cramped. Maximise info per viewport. |
15| Clear hierarchy | Strong contrast between labels, content, and muted elements |
16| Functional spacing | 8px on items, 12–16px on sections. No wasted space. |
17| Keyboard-first | Every action reachable without a mouse |
18| Dark-mode primary | Developers live in dark mode. Light mode is supported, secondary. |
19
20**Do:** small type (`text-xs` / `text-sm` for most UI), small buttons, monospace for
21paths, SHAs, and branch names, opacity modifiers for text hierarchy, tight spacing.
22
23**Don't:** large type outside page titles, shadows in dark mode (use borders), bright
24backgrounds, hover animations that move or resize things.
25
26## Where the portfolio framing bites
27
28Steid is portfolio-first, not a Gitea clone. The profile page is the product — repos
29are one kind of thing on it, alongside writing and projects. Any layout inherited from
30a GitHub-shaped forge needs checking against that before it's copied.
31
32## The shell
33
34Settled 2026-09-05, when app-shell navigation (PostHog/GitLab-style sidebar) was
35considered and rejected.
36
37**A slim top bar plus contextual tabs, not an app shell.** The reasoning:
38
39- Sidebar shells optimise for the logged-in daily user hopping between many top-level
40 destinations. Steid's first audience is the opposite — an anonymous reader of
41 someone's site who will never sign in — and rail chrome reads as "self-hosted admin
42 tool", which is the exact wrong first impression for a portfolio. The managed
43 offering strengthens this: every paying customer's audience is that visitor.
44- Steid has four-ish global destinations; a sidebar would be mostly empty rail,
45 advertising incompleteness the same way the old stacked-section profile did.
46- A shell-for-owners / site-for-visitors split is the two-design-languages trap again.
47 One vibe.
48
49What ships instead:
50
51- **The top bar**: one ~44px row, wordmark on the left → `/`, sign-in state on the
52 right ("Sign in", or `@handle` / Settings / Sign out). It never grows navigation;
53 that belongs to the thing being navigated — the profile's tab bar, the repository's.
54- **Per-page widths.** The layout imposes none; pages wrap themselves in `narrow`
55 (48rem — prose-shaped: profile, forms, settings, writing) or `wide` (80rem —
56 code-shaped: tree, blob, log). Both live in `layout.rs`. A page that forgets one
57 renders full-bleed, which is visible, not silently wrong.
58- Where a sidebar does earn its place is *within* a wide page — a file tree beside a
59 blob, a settings-section nav — as local structure, not global chrome.
60
61Open: **the wordmark says "steid"**, but on someone's own domain, portfolio-first
62argues the top-left identity slot belongs to the owner, not to Steid. Undecided;
63becomes decidable when multi-user lands and "the owner" needs a rule anyway. The
64larger shape of it (noted 2026-09-05): companies and individuals — self-hosted or
65managed — will likely want **custom text in that slot, and perhaps elsewhere**: a
66company name where "steid" sits, maybe a footer line. That is an instance-level
67setting on `Organization` or config, not a theme; whatever answers the wordmark
68question should leave room for it rather than hardcoding either choice.
69
70## The repository page
71
72Settled 2026-09-05. `/{handle}/repos/{name}` is the only two-column page in a
73repository: code on the left, an About sidebar on the right. Every page below it —
74a tree subpath, a file, the log — stays a single `wide` column, the same way GitHub
75narrows once you are inside the tree.
76
77```
78@jamesgill /
79steid [Settings]
80Code Commits
81──────────────────────────────────────────────────────────────────────────
82main ▾ ⑂ 3 branches · 2 tags │ ABOUT
83● docs: record the forge feature survey │ A personal-first gitforge…
84 31a6a7e · 2d │ 🕮 AGPL-3.0
85┌──────────────────────────────────────┐ │ ─────────────────────────
86│ steid │ │ Commits 80
87│ deploy/ plans/ src/ │ │ Branches 3
88│ Cargo.toml README.md LICENSE │ │ Tags 2
89└──────────────────────────────────────┘ │ Latest tag ⌗ v0.2.0
90┌──────────────────────────────────────┐ │ Pushed 2 days ago
91│ README.md │ │ ─────────────────────────
92└──────────────────────────────────────┘ │ CLONE
93 │ https://…/steid.git
94```
95
96**The sidebar's About block is the portfolio pitch.** A visitor arriving from a
97profile reads it before they read any code — what this is, whether they may use it,
98how much of it there is — so it gets the right-hand column and the description gets
99its only home there. The header stays one line tall as a result: two homes for a
100description means the header grows tallest on exactly the repositories with the most
101to say, pushing the code below the fold.
102
103- **The header and tab strip are shared by every repository page**, so a tree, a file
104 and a log read as one place rather than three. The strip takes the active tab as a
105 parameter, so a new tab is one variant and one line.
106- **The active tab is underlined in the primary colour.** Nothing else on the strip
107 is coloured, so it reads as position rather than decoration. The only other use of
108 the primary colour on the page is the latest-commit dot.
109- **The latest-commit bar stands in for the per-file last-commit column** that
110 [0006]decisions/0006-git-binary-behind-narrow-ports.md defers. It says the same
111 thing once instead of once per row, for one `git log --max-count=1`.
112- **Counts beside the switcher are text, not links.** `/branches` and `/tags` do not
113 exist; a dead link is worse than a number.
114- **Sidebar sections are separated by hairlines, never boxed as cards** — the rule the
115 profile page settled on, and what makes the page read as one surface. Below `lg` the
116 sidebar drops beneath the file list and grows a rule of its own, so it reads as a new
117 section rather than as more of the listing.
118- **A licence is named only when its own text names it.** A licence file Steid cannot
119 identify is linked to and labelled "Licence": naming the wrong one is a claim about
120 somebody's legal terms.
121
122### Where the next features go
123
124The slots matter as much as what fills them today. Written down so the next feature
125lands in the frame rather than beside it:
126
127| Feature | Where it goes |
128|---|---|
129| Issues, Pull requests | a tab each, beside Code and Commits |
130| A commit page | the sha in the latest-commit bar and in the log |
131| Branches, Tags | the counts beside the switcher become links |
132| Compare | reached from a branch row on the branches page |
133| Blame | a toggle in the blob's own header, beside Raw |
134| Archive download | two small links under the clone URL |
135| Code search | a box on the right of the toolbar row, which is empty for it |
136| Per-file last commit | a column in the listing, once `cat-file --batch` is kept alive |
137
138**The cost is known and accepted.** The landing page makes **15 `git` processes** for a
139repository with a README and a licence, 10 without them, and 1 for an empty one — up
140from 7. Five of those are the sidebar's, run concurrently. That is the bill 0006 said
141would come due; it comes due at a kept-alive `cat-file --batch`, not before.
142
143## The profile page
144
145Settled 2026-08-29, after looking at the built page with real data rather than
146imagining it. Three sections of equal weight — Repositories, Writing, Projects — two of
147them empty boxes, repositories sorted alphabetically so `dotfiles` outranked `steid`.
148A third of the page was content and two thirds advertised incompleteness.
149
150**Flat, app-style navigation. No cards, no boxes.**
151
152```
153James Gill
154@jamesgill
155<bio>
156<links>
157─────────────────────────────────────────────
158Overview Repositories 3 Writing
159─────────────────────────────────────────────
160CURRENTLY BUILDING
161steid Rust
162A self-hostable gitforge where the profile is the product.
163421 tests · updated 2 hours ago
164
165REPOSITORIES All 3 →
166──────────────────────────────────────────
167topcoat-notes 3d
168Notes on building with Topcoat 0.5.
169──────────────────────────────────────────
170dotfiles 2w
171```
172
173- **A tab bar, not stacked sections.** Empty sections stop being visible failures and
174 become destinations that simply have nothing in them yet. It is also what makes
175 Writing shippable without redesigning the page around it.
176- **Tabs are links.** No JavaScript, and each tab is a real URL that can be shared.
177- **One lead item with genuine typographic weight**, then a compact list. This is the
178 editorial layer portfolio-first requires: without it the page has no opinion about
179 what matters, and ordering alone cannot carry that.
180- **Hairlines and hover states, never bordered containers.** The row is the unit.
181- **Recency, not alphabetical.** Alphabetical is a filing rule; a portfolio needs an
182 editorial one.
183- **Sections with nothing in them are hidden from visitors** and shown to the owner as
184 an affordance.
185
186### One vibe. Hierarchy without scale.
187
188The first version of this design gave the lead item large type and justified it by
189splitting the product into a "portfolio surface" and a "tool surface" with different
190rules. **That was wrong and is not what ships.** Two design languages in one product is
191how a product stops feeling like one thing, and it contradicted this file's own
192philosophy — quiet confidence, the UI disappears, small type throughout.
193
194Nothing exceeds 14px except the page title. The lead item is distinguished by
195**everything except size**:
196
197| Device | Lead | List row |
198|---|---|---|
199| Eyebrow label | yes | no |
200| Weight | 600 | 500 |
201| Description | full width, roomy | compact |
202| Space around it | generous | tight |
203| Separator | hairline below | none |
204
205The cost, stated plainly: the distinction is **subtle**. One weight step is not much,
206and the eyebrow and whitespace carry most of it. That is the right trade for a developer
207tool — but it means the copy in the eyebrow is load-bearing, because it is the clearest
208signal that this item is different.
209
210**The eyebrow reads "Currently building"**, chosen over a neutral "Featured". It says
211something rather than labelling something, which is what a portfolio built in public
212wants. The known cost: it is a claim about the present, so pinning a finished project
213makes the page lie. The answer if that becomes a problem is a per-pin label, not a
214blander default.
215
216The general rule this sets: **hierarchy comes from weight, colour and space. Reach for
217size last, and outside a page title, probably not at all.**
218
219### The front door
220
221Steid is meant to be someone's site, reached at their own domain — `jpgill.dev`, not
222`git.jpgill.dev`, because a `git.` subdomain announces a Gitea clone and this is
223portfolio-first. That makes the root a signpost rather than a page: unclaimed it sends
224you to setup, and otherwise it forwards to a profile — your own when signed in, **the
225owner's when not**.
226
227The last case is the one that matters. A stranger arriving at the apex came for the
228profile and will never sign in, so a sign-in prompt there is the front door answering the
229wrong question. It only applies when the owner is unambiguous: with more than one user
230"the owner" has no answer, so the root falls back to a generic landing rather than
231electing someone by storage order.
232
233### What it needs that does not exist yet
234
235- **`updated_at` on a repository.** The design promises recency and nothing stores it.
236 Asking git costs one fork per repository — a twelve-repo profile would be ~150ms of
237 `execve` before rendering. So it is a column, touched when a push is authorized.
238 Slightly wrong if a push then fails; the alternative is wrong more expensively.
239- **A `/{handle}/repos` index.** The Repositories tab needs a destination; only
240 `/{handle}/repos/{name}` exists.
241- **A way to choose the lead.** A `pinned` flag on the repository, set from the settings
242 page that now exists. No pin, no lead section — automatic "most recent" would put a
243 dotfiles tweak at the top of a portfolio.
244
245### Mocking is harder than it looks here
246
247**Tailwind classes the app does not already use are not in the built stylesheet.**
248`build.rs` scans the real sources, so a mockup written against the served CSS silently
249loses any new utility — `gap-7` and `tracking-widest` collapsed a nav into
250`OverviewRepositories3WritingProjects` before this was understood. Iterate either in the
251app itself or, for throwaway exploration, in plain CSS against the theme's custom
252properties (`--background`, `--foreground`, `--muted-foreground`, `--border`, `--surface`).
253
254Dark mode is a `.dark` class on an ancestor, not `prefers-color-scheme`; a mockup
255without it renders light.
256
257## Stack
258
259Settled in [0005]decisions/0005-tailwind-and-copied-components.md.
260
261| Concern | Choice |
262|---|---|
263| Styling | Tailwind via Topcoat's build script — no Node |
264| Theme | `styles.css` at the package root: design tokens, dark-first |
265| Components | `topcoat ui add` for primitives, hand-written for Steid's own |
266| Mono | IBM Plex Mono, for paths, hashes, branches, handles |
267
268### Tokens
269
270Defined in `styles.css` on `:root` (light) and `.dark` (dark, the default):
271
272```
273background surface foreground muted-foreground
274primary primary-foreground
275success success-foreground confirmations, git additions
276warning warning-foreground caution, git modifications
277destructive destructive-foreground errors, git deletions
278border ring shadow-xs shadow-sm
279```
280
281`surface`, `success` and `warning` are Steid's additions to Topcoat's `neutral` theme;
282the rest were retuned to the palette below.
283
284**Components reference tokens, never raw colours.** A hardcoded colour will not follow
285a palette change and will not adapt to the colour scheme. This is the convention to
286enforce in review.
287
288### Working on it
289
290```bash
291topcoat dev # rebuilds CSS and re-bundles on change
292topcoat asset bundle # manual builds only; a stale bundle serves stale CSS
293topcoat ui list # what the registry offers
294topcoat ui add <name> # copies source into src/components/
295```
296
297`components.toml` is the record of what is installed; don't duplicate that list here.
298
299**A registry component can need more than a copy.** `select` draws its chevron with an
300Iconify icon, which needs the `icon-iconify` feature on `topcoat` *and* the icon set
301staged in `build.rs`:
302
303```rust
304topcoat::icon::iconify::BuildConfig::new().icon_set("feather").stage().unwrap();
305```
306
307The build fails with a message naming the missing set, so it is discoverable — but the
308build.rs edit is easy not to expect from a command that only advertises copying a file.
309Icons are embedded at build time; nothing is fetched at runtime.
310
311`topcoat ui add` also rewrites the module list in `src/components.rs` by appending, so
312re-alphabetise it afterwards.
313
314## Prior art
315
316Attempt #1 (`steid-backup/AGENTS/UI.md`) has a complete 643-line design system —
317OKLCH light/dark palettes with concrete token values, a type scale, spacing scale, and
318component markup for sidebar, file tree, commit bar, breadcrumbs, badges, empty
319states. It was written for Tailwind + DaisyUI.
320
321Its palette has been mined already — the dark-first OKLCH values at hue 260 are what
322`styles.css` was retuned to. Its component markup has not, and comes with two caveats:
323it specifies DaisyUI, which is a separate choice from Tailwind and not bundled by
324Topcoat; and it was written for a GitHub-shaped forge rather than a portfolio-first
325one.
326
327It is also, on its own, longer than every other doc in this directory combined — for
328an app that had about nine pages. Take the palette and the principles. Don't
329re-specify components before there are components.