# UI

## Philosophy

A developer tool people use daily to browse code and manage repos. Optimise for:

1. **Speed-first** — minimise clicks, maximise information density
2. **Scannable** — find what you need in under a second
3. **Quiet confidence** — premium without flashy; the UI should disappear
4. **Code-centric** — code is the hero, everything else supports it

| Principle | Meaning |
|---|---|
| Readable density | Compact without cramped. Maximise info per viewport. |
| Clear hierarchy | Strong contrast between labels, content, and muted elements |
| Functional spacing | 8px on items, 12–16px on sections. No wasted space. |
| Keyboard-first | Every action reachable without a mouse |
| Dark-mode primary | Developers live in dark mode. Light mode is supported, secondary. |

**Do:** small type (`text-xs` / `text-sm` for most UI), small buttons, monospace for
paths, SHAs, and branch names, opacity modifiers for text hierarchy, tight spacing.

**Don't:** large type outside page titles, shadows in dark mode (use borders), bright
backgrounds, hover animations that move or resize things.

## Where the portfolio framing bites

Steid is portfolio-first, not a Gitea clone. The profile page is the product — repos
are one kind of thing on it, alongside writing and projects. Any layout inherited from
a GitHub-shaped forge needs checking against that before it's copied.

## The profile page

Settled 2026-08-29, after looking at the built page with real data rather than
imagining it. Three sections of equal weight — Repositories, Writing, Projects — two of
them empty boxes, repositories sorted alphabetically so `dotfiles` outranked `steid`.
A third of the page was content and two thirds advertised incompleteness.

**Flat, app-style navigation. No cards, no boxes.**

```
James Gill
@jamesgill
<bio>
<links>
─────────────────────────────────────────────
Overview   Repositories 3   Writing
─────────────────────────────────────────────
CURRENTLY BUILDING
steid  Rust
A self-hostable gitforge where the profile is the product.
421 tests · updated 2 hours ago

REPOSITORIES                                All 3 →
──────────────────────────────────────────
topcoat-notes                                    3d
Notes on building with Topcoat 0.5.
──────────────────────────────────────────
dotfiles                                         2w
```

- **A tab bar, not stacked sections.** Empty sections stop being visible failures and
  become destinations that simply have nothing in them yet. It is also what makes
  Writing shippable without redesigning the page around it.
- **Tabs are links.** No JavaScript, and each tab is a real URL that can be shared.
- **One lead item with genuine typographic weight**, then a compact list. This is the
  editorial layer portfolio-first requires: without it the page has no opinion about
  what matters, and ordering alone cannot carry that.
- **Hairlines and hover states, never bordered containers.** The row is the unit.
- **Recency, not alphabetical.** Alphabetical is a filing rule; a portfolio needs an
  editorial one.
- **Sections with nothing in them are hidden from visitors** and shown to the owner as
  an affordance.

### One vibe. Hierarchy without scale.

The first version of this design gave the lead item large type and justified it by
splitting the product into a "portfolio surface" and a "tool surface" with different
rules. **That was wrong and is not what ships.** Two design languages in one product is
how a product stops feeling like one thing, and it contradicted this file's own
philosophy — quiet confidence, the UI disappears, small type throughout.

Nothing exceeds 14px except the page title. The lead item is distinguished by
**everything except size**:

| Device | Lead | List row |
|---|---|---|
| Eyebrow label | yes | no |
| Weight | 600 | 500 |
| Description | full width, roomy | compact |
| Space around it | generous | tight |
| Separator | hairline below | none |

The cost, stated plainly: the distinction is **subtle**. One weight step is not much,
and the eyebrow and whitespace carry most of it. That is the right trade for a developer
tool — but it means the copy in the eyebrow is load-bearing, because it is the clearest
signal that this item is different.

**The eyebrow reads "Currently building"**, chosen over a neutral "Featured". It says
something rather than labelling something, which is what a portfolio built in public
wants. The known cost: it is a claim about the present, so pinning a finished project
makes the page lie. The answer if that becomes a problem is a per-pin label, not a
blander default.

The general rule this sets: **hierarchy comes from weight, colour and space. Reach for
size last, and outside a page title, probably not at all.**

### The front door

Steid is meant to be someone's site, reached at their own domain — `jpgill.dev`, not
`git.jpgill.dev`, because a `git.` subdomain announces a Gitea clone and this is
portfolio-first. That makes the root a signpost rather than a page: unclaimed it sends
you to setup, and otherwise it forwards to a profile — your own when signed in, **the
owner's when not**.

The last case is the one that matters. A stranger arriving at the apex came for the
profile and will never sign in, so a sign-in prompt there is the front door answering the
wrong question. It only applies when the owner is unambiguous: with more than one user
"the owner" has no answer, so the root falls back to a generic landing rather than
electing someone by storage order.

### What it needs that does not exist yet

- **`updated_at` on a repository.** The design promises recency and nothing stores it.
  Asking git costs one fork per repository — a twelve-repo profile would be ~150ms of
  `execve` before rendering. So it is a column, touched when a push is authorized.
  Slightly wrong if a push then fails; the alternative is wrong more expensively.
- **A `/{handle}/repos` index.** The Repositories tab needs a destination; only
  `/{handle}/repos/{name}` exists.
- **A way to choose the lead.** A `pinned` flag on the repository, set from the settings
  page that now exists. No pin, no lead section — automatic "most recent" would put a
  dotfiles tweak at the top of a portfolio.

### Mocking is harder than it looks here

**Tailwind classes the app does not already use are not in the built stylesheet.**
`build.rs` scans the real sources, so a mockup written against the served CSS silently
loses any new utility — `gap-7` and `tracking-widest` collapsed a nav into
`OverviewRepositories3WritingProjects` before this was understood. Iterate either in the
app itself or, for throwaway exploration, in plain CSS against the theme's custom
properties (`--background`, `--foreground`, `--muted-foreground`, `--border`, `--surface`).

Dark mode is a `.dark` class on an ancestor, not `prefers-color-scheme`; a mockup
without it renders light.

## Stack

Settled in [0005](decisions/0005-tailwind-and-copied-components.md).

| Concern | Choice |
|---|---|
| Styling | Tailwind via Topcoat's build script — no Node |
| Theme | `styles.css` at the package root: design tokens, dark-first |
| Components | `topcoat ui add` for primitives, hand-written for Steid's own |
| Mono | IBM Plex Mono, for paths, hashes, branches, handles |

### Tokens

Defined in `styles.css` on `:root` (light) and `.dark` (dark, the default):

```
background  surface  foreground  muted-foreground
primary     primary-foreground
success     success-foreground      confirmations, git additions
warning     warning-foreground      caution, git modifications
destructive destructive-foreground  errors, git deletions
border      ring      shadow-xs  shadow-sm
```

`surface`, `success` and `warning` are Steid's additions to Topcoat's `neutral` theme;
the rest were retuned to the palette below.

**Components reference tokens, never raw colours.** A hardcoded colour will not follow
a palette change and will not adapt to the colour scheme. This is the convention to
enforce in review.

### Working on it

```bash
topcoat dev              # rebuilds CSS and re-bundles on change
topcoat asset bundle     # manual builds only; a stale bundle serves stale CSS
topcoat ui list          # what the registry offers
topcoat ui add <name>    # copies source into src/components/
```

`components.toml` is the record of what is installed; don't duplicate that list here.

**A registry component can need more than a copy.** `select` draws its chevron with an
Iconify icon, which needs the `icon-iconify` feature on `topcoat` *and* the icon set
staged in `build.rs`:

```rust
topcoat::icon::iconify::BuildConfig::new().icon_set("feather").stage().unwrap();
```

The build fails with a message naming the missing set, so it is discoverable — but the
build.rs edit is easy not to expect from a command that only advertises copying a file.
Icons are embedded at build time; nothing is fetched at runtime.

`topcoat ui add` also rewrites the module list in `src/components.rs` by appending, so
re-alphabetise it afterwards.

## Prior art

Attempt #1 (`steid-backup/AGENTS/UI.md`) has a complete 643-line design system —
OKLCH light/dark palettes with concrete token values, a type scale, spacing scale, and
component markup for sidebar, file tree, commit bar, breadcrumbs, badges, empty
states. It was written for Tailwind + DaisyUI.

Its palette has been mined already — the dark-first OKLCH values at hue 260 are what
`styles.css` was retuned to. Its component markup has not, and comes with two caveats:
it specifies DaisyUI, which is a separate choice from Tailwind and not bundled by
Topcoat; and it was written for a GitHub-shaped forge rather than a portfolio-first
one.

It is also, on its own, longer than every other doc in this directory combined — for
an app that had about nine pages. Take the palette and the principles. Don't
re-specify components before there are components.
