add the interface half of the duck suite design language
DuckSuite_Design_Language.md covers icons only — mascot geometry, lighting, one-idea-per-icon, readable at 64px. It says nothing about type, density or data, which is why the chrome looks considered and every data view does not. This is the companion document plus the primitives that enforce it. The diagnosis it is written against, from grepping two apps: 11 uses of text-[10px], 7 of text-[11px], 52 of text-xs and 3 of text-base, with no rule about which meant what; six radius values; hand-picked emerald/amber/red/purple next to unused --success/--warning/--destructive tokens; text-black/50 and ring-black/5, which are invisible in dark mode. None of that was a bad decision, it was the absence of one thirty times over — so the fix is to remove the choice rather than to have better taste. docs/design-language-interface.md sets four type ranks with a 12px floor, one focal point per row, five state tones, three radii and a spacing rhythm. The principles are lifted from the icon language rather than invented, because "one focal point, reads instantly, no unnecessary decorations, if it needs explanation it is too complicated" is already the right rule for a dense list. components/Data/ is how you spend that vocabulary: DataRow/DataList/RowMeta, StatusPill/StatusIcon, LoadingBlock/ErrorBlock/EmptyBlock, RelativeTime. Rules you have to remember are rules thirty views already broke, so the shape encodes them — DataRow takes exactly one title and everything else is meta, RowMeta puts separators between items so a trailing dot cannot appear, RelativeTime carries the absolute timestamp as a hover title. Adds --info (violet) as the fifth semantic tone, light and dark. "Merged" and "in progress" are neither good news nor bad, and painting them with --success makes a merged PR and an open one look like the same thing. Entirely additive: nothing imports these yet, so no existing view changes and there is no collision with the other agent working in this tree. Typechecks clean. Not yet rendered in a browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# 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-<tone>/15` with `text-<tone>`. 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.
|
||||
Reference in New Issue
Block a user