steid

@jamesgill /

steid/plans/ui.md
20.1 KBCode·Blame·Raw
ab7fea9chore: plans setup1mo
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
5d3dfa5feat: a global top bar, and pages choose their own width23h
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;
6d38c90docs: the wordmark slot is future custom branding, not just an identity question19h
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.
5d3dfa5feat: a global top bar, and pages choose their own width23h
69
0aca94efeat: a repository is one place, and its landing page says what it is18h
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`.
9b1feb6docs: fold the branches and tags handover into plans18h
112- **Counts beside the switcher link to `/branches` and `/tags`.** Both pages sit under
113 the Code tab — they are ways into the code, not places of their own, and a tab each
114 for two lists would make the strip advertise plumbing. Each page's heading cross-links
115 the other. Their rows keep trailing metadata in fixed-width right-aligned columns so
116 the sha does not zig-zag, and below `sm` drop the subject and Compare link rather than
117 wrap: a row is a scanning surface. Three distinct empty states — no commits (push
118 snippet), no tags yet (`git push --tags`), and a timeout that says the repository is fine.
0aca94efeat: a repository is one place, and its landing page says what it is18h
119- **Sidebar sections are separated by hairlines, never boxed as cards** — the rule the
7aae36cfix: on a narrow viewport the About sidebar comes first, not last18h
120 profile page settled on, and what makes the page read as one surface.
121- **Below `lg` the sidebar is not a sidebar: it comes first**, above the file list, with
122 a rule under it and no stickiness. Ordering it after the code would bury "what is this
123 and may I use it" under a long README — the one question the visitor arrived with, on
124 the viewport with the least room to go looking for it. That is why the two-column
125 container is a flex column rather than a plain block: `order-first` needs a flex
126 parent.
0aca94efeat: a repository is one place, and its landing page says what it is18h
127- **A licence is named only when its own text names it.** A licence file Steid cannot
128 identify is linked to and labelled "Licence": naming the wrong one is a claim about
129 somebody's legal terms.
130
6f0a608docs: fold the highlighting handover into plans18h
131- **Syntax colour is the one place colour carries information rather than position.**
132 The palette is deliberately narrow: comments below muted-foreground, strings and
133 keywords carrying the contrast, punctuation just under foreground, everything else
134 close to it — a file reads as text with structure, not a parade. It is one
135 `--syntax-*` block in `styles.css`, so it follows the palette. The over-cap notice is a
136 row of the blob panel sharing the header's hairline, a page state rather than a banner.
137
4e4ba31docs: fold the archive and search handover into plans18h
138- **Download links are for the revision being viewed**, under the clone URL; someone
139 reading a tag wants that tag's tarball. An empty repository shows none.
140- **The search box is the width of the About sidebar** on `sm` and up, so the two columns
141 line up; full width below and on the results page. `<mark>` is `bg-primary/25`, the only
142 other primary on the page besides the active tab and the latest-commit dot: it marks
143 the thing being looked for, the same rule. Every search state is a bordered panel with
144 one sentence. A long matched line scrolls inside its own `<code>`.
145
64f6f0ddocs: fold the commit page and compare handover into plans17h
146- **A commit page is a Commits-tab page**, and its Commits link points at the log at
147 that commit's own id, not the revision in the URL: a branch name there would send
148 someone to a different commit tomorrow. **A compare page is a Code-tab page**: it is
149 about two revisions of the code, reached from the branches page and also directly at
150 `/compare`.
151- **Success and destructive appear as text in exactly three places**: the commit's
152 `+a −b`, each file header's, and the truncated file list. Diff line tints are the same
153 tokens at 12% (rows) and 20% (gutters) through `color-mix` in plain CSS. The primary
154 colour is used only for links out of a dead end ("View the whole file", "Compare them
155 the other way round").
156
691bf98docs: fold the blame handover into plans17h
157- **A file has two views and one header.** `Code · Blame · Raw` sits where Raw alone
158 used to, the active view in the primary colour, the same rule as the tab strip. Raw is
159 never active: it downloads. **Blame's rows are the blob's rows** — same type, leading
160 and height, so they read as two readings of one thing; the commit is shown once per run
161 and held to a single line. **The age tint is texture, not a heat map**: a hairline down
162 the left of each run at 5–30% of the primary. If it ever reads as a value to look up, it
163 is too strong.
164
0aca94efeat: a repository is one place, and its landing page says what it is18h
165### Where the next features go
166
167The slots matter as much as what fills them today. Written down so the next feature
168lands in the frame rather than beside it:
169
170| Feature | Where it goes |
171|---|---|
172| Issues, Pull requests | a tab each, beside Code and Commits |
64f6f0ddocs: fold the commit page and compare handover into plans17h
173| A commit page | done — the sha in the latest-commit bar and in the log |
9b1feb6docs: fold the branches and tags handover into plans18h
174| Branches, Tags | done — the counts beside the switcher and in the sidebar |
64f6f0ddocs: fold the commit page and compare handover into plans17h
175| Compare | done — a branch row on the branches page, or `/compare` directly |
691bf98docs: fold the blame handover into plans17h
176| Blame | done — the toggle in the file header, both views |
4e4ba31docs: fold the archive and search handover into plans18h
177| Archive download | done — two small links under the clone URL |
178| Code search | done — the box on the right of the toolbar row |
0aca94efeat: a repository is one place, and its landing page says what it is18h
179| Per-file last commit | a column in the listing, once `cat-file --batch` is kept alive |
180
181**The cost is known and accepted.** The landing page makes **15 `git` processes** for a
182repository with a README and a licence, 10 without them, and 1 for an empty one — up
183from 7. Five of those are the sidebar's, run concurrently. That is the bill 0006 said
184would come due; it comes due at a kept-alive `cat-file --batch`, not before.
185
f0afae5docs: settle the profile page on flat navigation7d
186## The profile page
187
188Settled 2026-08-29, after looking at the built page with real data rather than
189imagining it. Three sections of equal weight — Repositories, Writing, Projects — two of
190them empty boxes, repositories sorted alphabetically so `dotfiles` outranked `steid`.
191A third of the page was content and two thirds advertised incompleteness.
192
193**Flat, app-style navigation. No cards, no boxes.**
194
195```
196James Gill
197@jamesgill
198<bio>
199<links>
200─────────────────────────────────────────────
201Overview Repositories 3 Writing
202─────────────────────────────────────────────
203CURRENTLY BUILDING
204steid Rust
205A self-hostable gitforge where the profile is the product.
206421 tests · updated 2 hours ago
207
208REPOSITORIES All 3 →
209──────────────────────────────────────────
210topcoat-notes 3d
211Notes on building with Topcoat 0.5.
212──────────────────────────────────────────
213dotfiles 2w
214```
215
216- **A tab bar, not stacked sections.** Empty sections stop being visible failures and
217 become destinations that simply have nothing in them yet. It is also what makes
218 Writing shippable without redesigning the page around it.
219- **Tabs are links.** No JavaScript, and each tab is a real URL that can be shared.
220- **One lead item with genuine typographic weight**, then a compact list. This is the
221 editorial layer portfolio-first requires: without it the page has no opinion about
222 what matters, and ordering alone cannot carry that.
223- **Hairlines and hover states, never bordered containers.** The row is the unit.
224- **Recency, not alphabetical.** Alphabetical is a filing rule; a portfolio needs an
225 editorial one.
226- **Sections with nothing in them are hidden from visitors** and shown to the owner as
227 an affordance.
228
30ba450docs: one design language, hierarchy without scale7d
229### One vibe. Hierarchy without scale.
230
231The first version of this design gave the lead item large type and justified it by
232splitting the product into a "portfolio surface" and a "tool surface" with different
233rules. **That was wrong and is not what ships.** Two design languages in one product is
234how a product stops feeling like one thing, and it contradicted this file's own
235philosophy — quiet confidence, the UI disappears, small type throughout.
236
237Nothing exceeds 14px except the page title. The lead item is distinguished by
238**everything except size**:
239
240| Device | Lead | List row |
241|---|---|---|
242| Eyebrow label | yes | no |
243| Weight | 600 | 500 |
244| Description | full width, roomy | compact |
245| Space around it | generous | tight |
246| Separator | hairline below | none |
247
248The cost, stated plainly: the distinction is **subtle**. One weight step is not much,
249and the eyebrow and whitespace carry most of it. That is the right trade for a developer
250tool — but it means the copy in the eyebrow is load-bearing, because it is the clearest
251signal that this item is different.
252
10b155cdocs: the profile eyebrow says "Currently building"7d
253**The eyebrow reads "Currently building"**, chosen over a neutral "Featured". It says
254something rather than labelling something, which is what a portfolio built in public
255wants. The known cost: it is a claim about the present, so pinning a finished project
256makes the page lie. The answer if that becomes a problem is a per-pin label, not a
257blander default.
258
30ba450docs: one design language, hierarchy without scale7d
259The general rule this sets: **hierarchy comes from weight, colour and space. Reach for
260size last, and outside a page title, probably not at all.**
f0afae5docs: settle the profile page on flat navigation7d
261
8ed4e5afeat: the root is the owner's profile1d
262### The front door
263
7da29e0fix: the domain is jpgill.dev, not jpgilldev.com1d
264Steid is meant to be someone's site, reached at their own domain — `jpgill.dev`, not
265`git.jpgill.dev`, because a `git.` subdomain announces a Gitea clone and this is
8ed4e5afeat: the root is the owner's profile1d
266portfolio-first. That makes the root a signpost rather than a page: unclaimed it sends
267you to setup, and otherwise it forwards to a profile — your own when signed in, **the
268owner's when not**.
269
270The last case is the one that matters. A stranger arriving at the apex came for the
271profile and will never sign in, so a sign-in prompt there is the front door answering the
272wrong question. It only applies when the owner is unambiguous: with more than one user
273"the owner" has no answer, so the root falls back to a generic landing rather than
274electing someone by storage order.
275
f0afae5docs: settle the profile page on flat navigation7d
276### What it needs that does not exist yet
277
278- **`updated_at` on a repository.** The design promises recency and nothing stores it.
279 Asking git costs one fork per repository — a twelve-repo profile would be ~150ms of
280 `execve` before rendering. So it is a column, touched when a push is authorized.
281 Slightly wrong if a push then fails; the alternative is wrong more expensively.
282- **A `/{handle}/repos` index.** The Repositories tab needs a destination; only
283 `/{handle}/repos/{name}` exists.
284- **A way to choose the lead.** A `pinned` flag on the repository, set from the settings
285 page that now exists. No pin, no lead section — automatic "most recent" would put a
286 dotfiles tweak at the top of a portfolio.
287
288### Mocking is harder than it looks here
289
290**Tailwind classes the app does not already use are not in the built stylesheet.**
291`build.rs` scans the real sources, so a mockup written against the served CSS silently
292loses any new utility — `gap-7` and `tracking-widest` collapsed a nav into
293`OverviewRepositories3WritingProjects` before this was understood. Iterate either in the
294app itself or, for throwaway exploration, in plain CSS against the theme's custom
295properties (`--background`, `--foreground`, `--muted-foreground`, `--border`, `--surface`).
296
297Dark mode is a `.dark` class on an ancestor, not `prefers-color-scheme`; a mockup
298without it renders light.
299
ab7fea9chore: plans setup1mo
300## Stack
301
af126c7feat: tailwind theme and the first components25d
302Settled in [0005]decisions/0005-tailwind-and-copied-components.md.
ab7fea9chore: plans setup1mo
303
af126c7feat: tailwind theme and the first components25d
304| Concern | Choice |
305|---|---|
306| Styling | Tailwind via Topcoat's build script — no Node |
307| Theme | `styles.css` at the package root: design tokens, dark-first |
308| Components | `topcoat ui add` for primitives, hand-written for Steid's own |
309| Mono | IBM Plex Mono, for paths, hashes, branches, handles |
ab7fea9chore: plans setup1mo
310
af126c7feat: tailwind theme and the first components25d
311### Tokens
ab7fea9chore: plans setup1mo
312
af126c7feat: tailwind theme and the first components25d
313Defined in `styles.css` on `:root` (light) and `.dark` (dark, the default):
314
315```
316background surface foreground muted-foreground
317primary primary-foreground
318success success-foreground confirmations, git additions
319warning warning-foreground caution, git modifications
320destructive destructive-foreground errors, git deletions
321border ring shadow-xs shadow-sm
322```
323
324`surface`, `success` and `warning` are Steid's additions to Topcoat's `neutral` theme;
325the rest were retuned to the palette below.
326
327**Components reference tokens, never raw colours.** A hardcoded colour will not follow
328a palette change and will not adapt to the colour scheme. This is the convention to
329enforce in review.
330
331### Working on it
ab7fea9chore: plans setup1mo
332
af126c7feat: tailwind theme and the first components25d
333```bash
334topcoat dev # rebuilds CSS and re-bundles on change
335topcoat asset bundle # manual builds only; a stale bundle serves stale CSS
336topcoat ui list # what the registry offers
337topcoat ui add <name> # copies source into src/components/
338```
ab7fea9chore: plans setup1mo
339
64222c3docs: record the icon staging trap and tick the repo-creation check24d
340`components.toml` is the record of what is installed; don't duplicate that list here.
341
342**A registry component can need more than a copy.** `select` draws its chevron with an
343Iconify icon, which needs the `icon-iconify` feature on `topcoat` *and* the icon set
344staged in `build.rs`:
345
346```rust
347topcoat::icon::iconify::BuildConfig::new().icon_set("feather").stage().unwrap();
348```
349
350The build fails with a message naming the missing set, so it is discoverable — but the
351build.rs edit is easy not to expect from a command that only advertises copying a file.
352Icons are embedded at build time; nothing is fetched at runtime.
353
354`topcoat ui add` also rewrites the module list in `src/components.rs` by appending, so
355re-alphabetise it afterwards.
356
ab7fea9chore: plans setup1mo
357## Prior art
358
359Attempt #1 (`steid-backup/AGENTS/UI.md`) has a complete 643-line design system —
360OKLCH light/dark palettes with concrete token values, a type scale, spacing scale, and
361component markup for sidebar, file tree, commit bar, breadcrumbs, badges, empty
362states. It was written for Tailwind + DaisyUI.
363
af126c7feat: tailwind theme and the first components25d
364Its palette has been mined already — the dark-first OKLCH values at hue 260 are what
365`styles.css` was retuned to. Its component markup has not, and comes with two caveats:
366it specifies DaisyUI, which is a separate choice from Tailwind and not bundled by
367Topcoat; and it was written for a GitHub-shaped forge rather than a portfolio-first
368one.
ab7fea9chore: plans setup1mo
369
370It is also, on its own, longer than every other doc in this directory combined — for
371an app that had about nine pages. Take the palette and the principles. Don't
372re-specify components before there are components.