steid

@jamesgill /

steid/plans/ui.md
11.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.
64
65## The profile page
66
67Settled 2026-08-29, after looking at the built page with real data rather than
68imagining it. Three sections of equal weight — Repositories, Writing, Projects — two of
69them empty boxes, repositories sorted alphabetically so `dotfiles` outranked `steid`.
70A third of the page was content and two thirds advertised incompleteness.
71
72**Flat, app-style navigation. No cards, no boxes.**
73
74```
75James Gill
76@jamesgill
77<bio>
78<links>
79─────────────────────────────────────────────
80Overview Repositories 3 Writing
81─────────────────────────────────────────────
82CURRENTLY BUILDING
83steid Rust
84A self-hostable gitforge where the profile is the product.
85421 tests · updated 2 hours ago
86
87REPOSITORIES All 3 →
88──────────────────────────────────────────
89topcoat-notes 3d
90Notes on building with Topcoat 0.5.
91──────────────────────────────────────────
92dotfiles 2w
93```
94
95- **A tab bar, not stacked sections.** Empty sections stop being visible failures and
96 become destinations that simply have nothing in them yet. It is also what makes
97 Writing shippable without redesigning the page around it.
98- **Tabs are links.** No JavaScript, and each tab is a real URL that can be shared.
99- **One lead item with genuine typographic weight**, then a compact list. This is the
100 editorial layer portfolio-first requires: without it the page has no opinion about
101 what matters, and ordering alone cannot carry that.
102- **Hairlines and hover states, never bordered containers.** The row is the unit.
103- **Recency, not alphabetical.** Alphabetical is a filing rule; a portfolio needs an
104 editorial one.
105- **Sections with nothing in them are hidden from visitors** and shown to the owner as
106 an affordance.
107
108### One vibe. Hierarchy without scale.
109
110The first version of this design gave the lead item large type and justified it by
111splitting the product into a "portfolio surface" and a "tool surface" with different
112rules. **That was wrong and is not what ships.** Two design languages in one product is
113how a product stops feeling like one thing, and it contradicted this file's own
114philosophy — quiet confidence, the UI disappears, small type throughout.
115
116Nothing exceeds 14px except the page title. The lead item is distinguished by
117**everything except size**:
118
119| Device | Lead | List row |
120|---|---|---|
121| Eyebrow label | yes | no |
122| Weight | 600 | 500 |
123| Description | full width, roomy | compact |
124| Space around it | generous | tight |
125| Separator | hairline below | none |
126
127The cost, stated plainly: the distinction is **subtle**. One weight step is not much,
128and the eyebrow and whitespace carry most of it. That is the right trade for a developer
129tool — but it means the copy in the eyebrow is load-bearing, because it is the clearest
130signal that this item is different.
131
132**The eyebrow reads "Currently building"**, chosen over a neutral "Featured". It says
133something rather than labelling something, which is what a portfolio built in public
134wants. The known cost: it is a claim about the present, so pinning a finished project
135makes the page lie. The answer if that becomes a problem is a per-pin label, not a
136blander default.
137
138The general rule this sets: **hierarchy comes from weight, colour and space. Reach for
139size last, and outside a page title, probably not at all.**
140
141### The front door
142
143Steid is meant to be someone's site, reached at their own domain — `jpgill.dev`, not
144`git.jpgill.dev`, because a `git.` subdomain announces a Gitea clone and this is
145portfolio-first. That makes the root a signpost rather than a page: unclaimed it sends
146you to setup, and otherwise it forwards to a profile — your own when signed in, **the
147owner's when not**.
148
149The last case is the one that matters. A stranger arriving at the apex came for the
150profile and will never sign in, so a sign-in prompt there is the front door answering the
151wrong question. It only applies when the owner is unambiguous: with more than one user
152"the owner" has no answer, so the root falls back to a generic landing rather than
153electing someone by storage order.
154
155### What it needs that does not exist yet
156
157- **`updated_at` on a repository.** The design promises recency and nothing stores it.
158 Asking git costs one fork per repository — a twelve-repo profile would be ~150ms of
159 `execve` before rendering. So it is a column, touched when a push is authorized.
160 Slightly wrong if a push then fails; the alternative is wrong more expensively.
161- **A `/{handle}/repos` index.** The Repositories tab needs a destination; only
162 `/{handle}/repos/{name}` exists.
163- **A way to choose the lead.** A `pinned` flag on the repository, set from the settings
164 page that now exists. No pin, no lead section — automatic "most recent" would put a
165 dotfiles tweak at the top of a portfolio.
166
167### Mocking is harder than it looks here
168
169**Tailwind classes the app does not already use are not in the built stylesheet.**
170`build.rs` scans the real sources, so a mockup written against the served CSS silently
171loses any new utility — `gap-7` and `tracking-widest` collapsed a nav into
172`OverviewRepositories3WritingProjects` before this was understood. Iterate either in the
173app itself or, for throwaway exploration, in plain CSS against the theme's custom
174properties (`--background`, `--foreground`, `--muted-foreground`, `--border`, `--surface`).
175
176Dark mode is a `.dark` class on an ancestor, not `prefers-color-scheme`; a mockup
177without it renders light.
178
179## Stack
180
181Settled in [0005]decisions/0005-tailwind-and-copied-components.md.
182
183| Concern | Choice |
184|---|---|
185| Styling | Tailwind via Topcoat's build script — no Node |
186| Theme | `styles.css` at the package root: design tokens, dark-first |
187| Components | `topcoat ui add` for primitives, hand-written for Steid's own |
188| Mono | IBM Plex Mono, for paths, hashes, branches, handles |
189
190### Tokens
191
192Defined in `styles.css` on `:root` (light) and `.dark` (dark, the default):
193
194```
195background surface foreground muted-foreground
196primary primary-foreground
197success success-foreground confirmations, git additions
198warning warning-foreground caution, git modifications
199destructive destructive-foreground errors, git deletions
200border ring shadow-xs shadow-sm
201```
202
203`surface`, `success` and `warning` are Steid's additions to Topcoat's `neutral` theme;
204the rest were retuned to the palette below.
205
206**Components reference tokens, never raw colours.** A hardcoded colour will not follow
207a palette change and will not adapt to the colour scheme. This is the convention to
208enforce in review.
209
210### Working on it
211
212```bash
213topcoat dev # rebuilds CSS and re-bundles on change
214topcoat asset bundle # manual builds only; a stale bundle serves stale CSS
215topcoat ui list # what the registry offers
216topcoat ui add <name> # copies source into src/components/
217```
218
219`components.toml` is the record of what is installed; don't duplicate that list here.
220
221**A registry component can need more than a copy.** `select` draws its chevron with an
222Iconify icon, which needs the `icon-iconify` feature on `topcoat` *and* the icon set
223staged in `build.rs`:
224
225```rust
226topcoat::icon::iconify::BuildConfig::new().icon_set("feather").stage().unwrap();
227```
228
229The build fails with a message naming the missing set, so it is discoverable — but the
230build.rs edit is easy not to expect from a command that only advertises copying a file.
231Icons are embedded at build time; nothing is fetched at runtime.
232
233`topcoat ui add` also rewrites the module list in `src/components.rs` by appending, so
234re-alphabetise it afterwards.
235
236## Prior art
237
238Attempt #1 (`steid-backup/AGENTS/UI.md`) has a complete 643-line design system —
239OKLCH light/dark palettes with concrete token values, a type scale, spacing scale, and
240component markup for sidebar, file tree, commit bar, breadcrumbs, badges, empty
241states. It was written for Tailwind + DaisyUI.
242
243Its palette has been mined already — the dark-first OKLCH values at hue 260 are what
244`styles.css` was retuned to. Its component markup has not, and comes with two caveats:
245it specifies DaisyUI, which is a separate choice from Tailwind and not bundled by
246Topcoat; and it was written for a GitHub-shaped forge rather than a portfolio-first
247one.
248
249It is also, on its own, longer than every other doc in this directory combined — for
250an app that had about nine pages. Take the palette and the principles. Don't
251re-specify components before there are components.