# DuckSuite Design Language — Interface The companion to `DuckSuite_Design_Language.md`, which covers icons. That document defines how a DuckSuite app looks **on a home screen**. This one defines how it looks **once opened**. They share principles, because the icon language's rules are good interface rules: > One focal point. Reads instantly. Readable at small sizes. No unnecessary decorations. > Minimal clutter. If it needs explanation, it is too complicated. This document governs **data surfaces** — lists, rows, tables, detail panes, anything rendering records from a service. It does not govern the chrome (dock, panels, workspace shell), which is settled and stays as it is. ## Why this exists Every data view in Officer was built on its own, and it shows. A survey of two apps found 11 uses of `text-[10px]`, 7 of `text-[11px]`, 52 of `text-xs` and 3 of `text-base` — a range of 10–16px with no rule about which meant what. Six radius values. Green used both for "this is open" and for "this is a heading icon", so green signalled nothing. None of that was a bad decision. It was the absence of one, thirty times over. The fix is not taste, it is removing the choice: **the ranks below are the whole vocabulary, and the primitives in `components/Data/` are how you spend it.** ## 1. Type — four ranks, and a floor | Rank | Class | Size | Use | | ------------- | ------------------------- | ---- | ------------------------------------------------------------ | | **Title** | `text-base font-semibold` | 16px | The one thing a screen is about. One per view. | | **Row title** | `text-sm font-medium` | 14px | The focal point of a row — repo name, issue title. | | **Body** | `text-sm` | 14px | Descriptions, prose, form values, anything read as language. | | **Meta** | `text-xs` | 12px | Secondary facts: timestamps, counts, authors, hints. | **12px is the floor.** No `text-[10px]`, no `text-[11px]`, no arbitrary sizes. If something feels like it needs to be smaller than meta, it is not important enough to be on the screen — cut it instead. This single rule is most of the readability fix. `font-medium` marks the focal point. `font-semibold` is for titles only. Never both in one row. ## 2. One focal point per row Straight from the icon language. A row has exactly **one** element at row-title rank; everything else is meta. A list is scanned, not read, and scanning works by finding the one bold thing per line. Two competing emphases per row and the eye has nowhere to land — which is why the current lists feel busy at a glance despite containing very little. Meta facts sit on **one line**, separated by a `·` dot, in source order of importance. They do not each get their own badge. ## 3. Colour means state, never decoration Colour on a data surface is **information**. If it isn't telling you something you'd act on, it is noise, and it dilutes the colour that is. Five tones, and no others: | Tone | Token | Means | | --------- | ------------- | --------------------------------------------------------------------- | | `neutral` | `muted` | No state, or a state with no valence. The default. | | `success` | `success` | Open, healthy, passing, connected, done. | | `warning` | `warning` | Needs attention, degraded, archived, stale. | | `danger` | `destructive` | Failed, rejected, closed-unresolved, error. | | `info` | `info` | In progress, merged, informational. Distinct from success on purpose. | Always the semantic token, never a raw Tailwind palette number. `emerald-500` and `success` may render the same pixel today, but only one of them follows the theme, survives dark mode, and can be re-tuned in one place. **`text-black/50` and `ring-black/5` are bugs** — they are invisible in dark mode. Fills are `bg-/15` with `text-`. Never a solid saturated fill on a small element. Brand colour (`brand`, the DuckSuite forest green) is for identity and primary actions. It is never a state. Keeping it out of the state vocabulary is what lets it read as "Officer" rather than as "good". ## 4. One radius rule | Shape | Radius | | -------------------------------- | -------------- | | Containers, cards, panels | `rounded-lg` | | Controls, inputs, buttons, tiles | `rounded-md` | | Pills, badges, avatars | `rounded-full` | Three values. Nothing else. A radius is not an expressive choice. ## 5. Spacing rhythm On a 4px grid, and each step means something: - `gap-1.5` — an icon and its own label. They are one object. - `gap-2` — items inside one cluster. - `gap-3` — between clusters in a row; the row's own `px-4 py-3`. - `gap-4` / `gap-6` — between sections. Dense list rows are `px-4 py-3`. Detail panes are `p-4` inside `gap-6` sections. Rows separate with `divide-y`, not with margins and not with a border per row. ## 6. The three non-content states are not an afterthought Every data surface can be loading, failed, or empty, and those are the states a new user sees first. They get the same care as the populated view, and they come from `StateBlock` so they are identical everywhere. An empty state says **what would be here** and **why it isn't** — never just "No results". An error state shows the real message, because the person reading it is usually the person who can fix it. ## 7. Numbers Anything columnar or comparable is `tabular-nums`. Counts, sizes, durations, dates. Digits that change width make a list twitch. ## Applying it Use `components/Data/` primitives rather than re-implementing a row. They encode everything above, which means adopting them is how a view complies — not a separate step afterwards. Adoption is per-view and additive; nothing breaks by not having adopted yet. Gitea went first because it is the first surface shown to people outside this machine.