hosts/docs/design-guide.md
phg 0b7b52521f feat #14: Implement design guide (#15)
Reviewed-on: #15
Co-authored-by: phg <mail@philip-henning.com>
Co-committed-by: phg <mail@philip-henning.com>
2026-09-04 17:15:39 +00:00

404 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TUI design guide
This guide is the normative design contract for the `hosts` terminal user
interface. It evolves the application's existing dark, keyboard-first,
master-detail interface into a consistent system without replacing its visual
identity.
The words **must**, **should**, and **may** distinguish requirements,
recommendations, and optional choices. The guide describes the target state;
current deviations are implementation work, not exceptions or user-reachable
features. User-facing documentation must continue to describe only behavior
that the application currently provides.
## Design character
`hosts` is a calm, trustworthy operations console. It should feel dense and
capable without feeling busy, playful, or alarming during ordinary use.
Every screen follows these principles:
1. **Information before decoration.** Structure, alignment, and concise text do
more work than borders, color, or ornament.
2. **Safety remains visible.** Mode, protection, failures, and consequences are
explicit and persist for as long as they matter.
3. **The keyboard path is complete.** Every workflow works without a mouse;
natural mouse support remains available where Textual provides it.
4. **State has more than one signal.** Color may reinforce a state but never
carries essential meaning alone.
5. **The terminal is user-owned.** The interface does not assume a particular
font, truecolor renderer, glyph width outside predictable one-cell symbols,
or a large viewport.
## Viewports and composition
The interface has three viewport modes:
| Terminal size | Contract |
| --- | --- |
| At least 120 columns by 40 rows | Design target. Show the full 60/40 master-detail workspace. |
| At least 100 columns by 30 rows | Minimum usable size. Keep the 60/40 workspace, compact the details, and abbreviate contextual controls without hiding required actions. |
| Below 100 columns or below 30 rows | Replace the workspace with a minimum-size presentation. Do not silently clip, overlap, or squeeze controls into unusable shapes. |
The target layout is:
```text
┌──────────────────────────── hosts ─────────────────────────────┐
│ message rail: transient result or persistent problem │
├─ Search ────────────────────────────────────────────────────────┤
│ query Filters: 2 │
├─ Host Entries (60%) ───────────────┬─ Entry Details (40%) ─────┤
│ state IP address hostname DNS │ IP address value │
│ ... │ Hostnames value │
│ │ DNS details value │
│ │ Comment — │
├────────────────────────────────────┴────────────────────────────┤
│ contextual keys count · filters · READ-ONLY │
└─────────────────────────────────────────────────────────────────┘
```
At the minimum usable size, preserve the same regions but use compact
label-value rows in Entry Details and shorter footer descriptions. If either
dimension falls below the minimum, render a stable presentation such as:
```text
┌──────────────────── hosts ────────────────────┐
│ Terminal too small │
│ │
│ hosts requires at least 100 columns × 30 rows │
│ Current size: 86 × 24 │
│ │
│ q Quit │
└───────────────────────────────────────────────┘
```
The minimum-size presentation must not permit a hidden or ambiguous mutation.
It may retain safe global actions such as Quit.
### Regions
- The header identifies the application and exposes safety-critical mode when
Privileged Mode is active.
- The message rail occupies one reserved row below the header. It never covers
the workspace.
- Search remains one row above the master-detail workspace.
- Host Entries and Entry Details use an approximate 60/40 split. Their minimum
content widths take priority over an exact percentage.
- The footer shows contextual keys on the left and durable state on the right.
It must not rely on horizontal scrolling.
## Theme and color
The application owns a semantic `hosts-dark` theme rather than inheriting
unspecified library defaults. Its initial palette preserves the current
Textual-dark appearance:
| Token | Initial dark value | Purpose |
| --- | --- | --- |
| `background` | `#121212` | Screen background |
| `surface` | `#1E1E1E` | Inputs and alternating table rows |
| `panel` | `#242F38` | Header, footer, and elevated containers |
| `foreground` | `#E0E0E0` | Primary text |
| `primary` | `#0178D4` | Structure, focus, and selection |
| `secondary` | `#004578` | Secondary structure |
| `accent` | `#FFA62B` | Key hints and exceptional emphasis |
| `success` | `#4EBF71` | Successful outcomes and confirmed healthy state |
| `warning` | `#FFA62B` | Caution, inactivity, and Privileged Mode emphasis |
| `error` | `#BA3C5B` | Failures and destructive actions |
Components must consume semantic tokens such as `$primary`, `$success`, and
`$text-muted`; they must not embed named Rich colors such as `green` or
`yellow`. Accent and warning happen to share an initial value but remain
separate roles.
Dark is the default appearance. A future light theme may choose different
values, but it must preserve the same semantic roles, hierarchy, and state
redundancy. Text should approximate a contrast ratio of at least 4.5:1 where
the renderer permits controlled color. Focus and meaningful large text should
approximate at least 3:1 against adjacent colors.
Use chromatic color sparingly:
- Primary blue describes structure, focus, and selection.
- Green describes a successful result or active mapping, never a generic
decoration.
- Amber describes caution, inactivity, or elevated privilege.
- Red describes failure or destructive consequence.
- Muted foreground describes secondary information, not information required
to finish a workflow.
## Text, symbols, and density
The terminal controls the font. Assume a legible monospace font and make no
layout or branding decision that depends on a particular typeface.
- Use bold for headings, keys, and the strongest current emphasis.
- Use dim text for supporting information.
- Use italics only as reinforcement; some terminals do not distinguish them.
- Use sentence case for headings, labels, buttons, and messages.
- Use one-cell spacing increments.
- Use **compact** density for tables, details, footers, and status areas.
- Use **comfortable** density for forms and modals so fields remain easy to
navigate and errors have room to appear.
Predictable one-cell Unicode symbols such as `✓`, `×`, and `!` may reinforce a
state. Emoji are not structural UI: their width, color, and availability vary
too widely. Essential meaning must survive if a symbol is absent or rendered
poorly. Details, help, and messages therefore use words in addition to any
marker.
## Borders and hierarchy
Rounded single-line borders define major regions: Search, Host Entries, and
Entry Details. Nested borders should not make every value compete with its
parent region. Within a region, prefer alignment, whitespace, text weight, and
subtle surface changes.
Inputs receive an input treatment only while they are editable. Read-only
values are label-value text, not disabled inputs. Thick borders are reserved
for modals; the error color is reserved for destructive confirmation and
failure states.
Good:
```text
╭─ Entry Details ─────────────────────╮
│ IP address 192.0.2.10 │
│ Hostnames api.local, api │
│ Comment — │
│ State ✓ Active │
╰─────────────────────────────────────╯
```
Avoid:
```text
╭─ Entry Details ─────────────────────╮
│ ╭─ IP Address ────────────────────╮ │
│ │ disabled input │ │
│ ╰────────────────────────────────╯ │
│ ╭─ Comment ───────────────────────╮ │
│ │ disabled input │ │
│ ╰────────────────────────────────╯ │
╰─────────────────────────────────────╯
```
## State grammar
Each interaction state has a distinct visual channel:
| State | Required treatment |
| --- | --- |
| Focus | Bright primary border or cursor that remains visible independently of selection |
| Selection | Contrasting row background; moving focus away must not erase which Host Entry is selected |
| Active Entry | Stable marker plus normal or success-toned text |
| Inactive Entry | Different marker plus warning-toned or muted text; italics may reinforce it |
| Default Entry | Explicit protected marker plus muted treatment and an explanation in Entry Details |
| Disabled action | Muted but still visible when hiding it would make a restriction mysterious |
| Work in progress | Text such as `Resolving…` plus an optional one-cell activity marker |
| Failure | Error token plus an actionable textual message |
| Privileged Mode | Persistent `PRIVILEGED` wording plus restrained warning emphasis |
Selection, focus, activity, and protection are independent. A selected
Inactive Entry must still look selected; a focused Default Entry must still
look protected.
## Core components
### Search and filters
The top Search field performs immediate text filtering. `Ctrl+F` opens advanced
criteria. Both surfaces edit one coherent filter state and must show the same
search term.
When non-text criteria are active, the workspace exposes a compact indicator,
such as `Filters: 2`. A no-results state says that filters matched no Host
Entries and presents the shortcut for changing or clearing them.
Placeholders may provide examples, but never replace a persistent label when a
field's meaning would otherwise disappear after typing.
### Host Entries table
The table is a scan surface, not a compressed details view. Preserve columns
under horizontal pressure in this order:
1. state;
2. IP address;
3. Canonical Hostname;
4. DNS status.
Never truncate an IP address. Long hostnames may end in an ellipsis; Entry
Details shows their complete values. Column widths remain stable while the
selection moves. DNS status uses a short, predictable marker in the table and
complete wording in Entry Details.
### Entry Details
Display inspection data as compact aligned label-value rows. Ordinary
IP-address Host Entries do not show empty DNS Name, DNS Status, or Last Resolved
rows. DNS Entry fields appear only when they apply. Use `—` for an absent
optional value such as a comment.
When no Host Entry is selected, say so rather than showing a field-shaped empty
screen. When no entries exist or no filter results match, name the condition
and offer the relevant next action.
### Entry Editor and forms
Forms use persistent labels, consistent required-field markers, and a visual
order that matches their Tab order. Validation appears directly beneath the
responsible field, retains the user's input, and explains how to correct the
value.
The primary action is visually prominent and last in keyboard order. Cancel is
always available and non-destructive. Arrow keys operate grouped controls such
as radio buttons; Tab and Shift+Tab move between fields and action groups.
### Modals
Modals are centered, bounded by the viewport, and share this structure:
1. concise title;
2. explanation or form content;
3. inline validation or consequence text;
4. action row.
Neutral and complex modals use the primary border. Only destructive
confirmation uses the error border. `Escape` cancels without mutation. Initial
focus lands on the safest sensible action; destructive confirmation never
defaults to the destructive button.
### Footer and help
The footer is contextual. It shows the small set of actions most relevant to
the current state rather than the complete binding catalog. Descriptions may
shorten in compact mode, but the keys and durable state remain visible. The
right side reports entry count, active filters, and either `READ-ONLY` or
`PRIVILEGED`.
`?` opens a dedicated overlay or screen instead of docking a panel that shrinks
the workspace. Group help under General, Navigation, Filtering, and Privileged
Mode. `?` and `Escape` close it and restore previous focus.
## Feedback and safety
The reserved message rail reports action results without covering content or
moving the master-detail workspace.
- Routine success and informational messages may expire.
- Errors, warnings, and unresolved safety conditions persist until the user
acknowledges them or a later result supersedes them.
- Durable state such as mode and active filters belongs in the footer.
- An error says what happened, whether state changed, and what the user can do
next.
Use the canonical terms from `CONTEXT.md`: **Read-only Mode**, **Privileged
Mode**, and **Entry Editor**. `Edit Mode` must not name Privileged Mode.
Entering Privileged Mode does not recolor the entire interface. A persistent,
warning-toned `PRIVILEGED` indicator communicates elevated capability without
normalizing alarm colors. Mutating actions become available contextually.
Default Entries have a visible protected marker and an explanation in Entry
Details. When a protected mutation is unavailable, keep the action visible if
hiding it would make the restriction unclear, and explain the restriction in
the message rail or help.
Confirm destructive operations and attempts to leave an Entry Editor with
unsaved changes. Name the affected Host Entry and initially focus the safest
sensible action. Routine navigation and inspection do not require
confirmation.
DNS resolution and other asynchronous work keep the interface responsive.
Show `Resolving…` inline for the affected DNS Entry. Completion or failure must
not move selection or steal focus unexpectedly.
Motion is optional, brief, and nonessential. It must not delay input, hide
state, or be necessary to understand an outcome.
## Input and focus
Every workflow must be keyboard-complete. Mouse support should work where
Textual provides it naturally, but no action may depend on hover or a pointer.
- Tab order follows visual reading order.
- Arrow keys operate tables and controls that conventionally use them.
- `Escape` backs out without mutation or opens the applicable unsaved-change
confirmation.
- Closing a modal or help restores focus to the control that opened it.
- Application shortcuts must not fire while a user is typing into a field
unless the shortcut is a conventional editing or escape command.
- Focus remains visible in dark, light, 256-color, and degraded-color output.
## Writing
English is the canonical interface language. Use concise, direct verbs and the
domain language in `CONTEXT.md`. Write complete strings and avoid layout
assumptions that make later localization needlessly difficult; an
internationalization architecture is outside the current contract.
Good and avoid:
| Good | Avoid | Reason |
| --- | --- | --- |
| `Enter Privileged Mode` | `Toggle edit mode` | Names the resulting safety state |
| `Delete Host Entry` | `Delete Entry!` | Uses the canonical concept without decorative alarm |
| `Save failed; previous state restored. Check write access and try again.` | `❌ Save error` | States outcome, data safety, and recovery |
| `No Host Entries match the current filters.` | `Nothing here` | Names the condition |
| `—` | `N/A` | Represents an absent optional value without implying an error |
## Accessibility and terminal compatibility
The supported visual baseline is a common 256-color terminal. Truecolor may
improve fidelity. Monochrome or degraded-color output may lose polish but must
retain all essential meaning through wording, markers, position, and text
style.
Do not depend on:
- color as the only state signal;
- italics, dim text, or animation as the only distinction;
- emoji or ambiguous-width glyphs;
- a particular terminal font;
- mouse hover;
- content hidden outside the supported viewport without a visible way to reach
it.
## Verification
Changes to the TUI must preserve behavior and essential visibility at 120×40
and 100×30. Tests should use Textual's test harness to cover:
- keyboard-complete workflows;
- visual-order focus traversal and focus restoration;
- selection remaining distinct from focus and entry state;
- mode, protection, validation, progress, and failure wording;
- conditional detail fields and empty states;
- absence of clipped or inaccessible essential controls at supported sizes.
Test structure and semantics rather than terminal pixels or exact glyph
rendering. Significant visual changes also receive an `agent-tui` inspection at
both supported sizes. Screenshots are illustrative evidence, not golden output.
Before review, check:
- Does the change preserve the calm operations-console character?
- Are all states legible without color?
- Is the keyboard path complete and ordered like the screen?
- Are canonical Hosts Management terms used?
- Does feedback remain visible for as long as it matters?
- Are 120×40 and 100×30 both usable without essential clipping?
- Does the full key reference remain reachable through `?`?
## Evolving the guide
This contract applies to the whole TUI, including existing screens. Track
current deviations in Forgejo rather than weakening the rules with permanent
exceptions.
An intentional departure must be explained in the relevant change. Update
this guide when the departure should become the new standard. Add an ADR only
when the decision is consequential, surprising, difficult to reverse, and the
result of a real trade-off.