From 3ce7ba8d5ca5772f3df6f04fd24cf2db1cae8a42 Mon Sep 17 00:00:00 2001 From: phg Date: Fri, 4 Sep 2026 16:44:21 +0200 Subject: [PATCH] Add TUI design guide and update contributing documentation --- AGENTS.md | 2 + CONTRIBUTING.md | 5 + docs/design-guide.md | 404 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 411 insertions(+) create mode 100644 docs/design-guide.md diff --git a/AGENTS.md b/AGENTS.md index dde8956..b0c89ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,8 @@ is reachable in the current application. privileged mode, sudo handling, system-file writes, or backups. - Read [ADR 0002](docs/adr/0002-normalize-hosts-serialization.md) before changing parsing, serialization, comment placement, or formatting preservation. +- Read the [TUI design guide](docs/design-guide.md) before changing layouts, + components, interaction states, user-visible copy, or terminal-size behavior. ## Commands diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b386303..d3c56be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,9 @@ Core code that is not reachable through the TUI is not yet a supported product feature. Do not advertise it in user-facing documentation until users can actually invoke it. +Read the [TUI design guide](docs/design-guide.md) before changing layouts, +components, interaction states, user-visible copy, or terminal-size behavior. + ## Domain language and decisions Read [CONTEXT.md](CONTEXT.md) before naming user-visible concepts. Use its @@ -111,6 +114,8 @@ Documentation is part of the behavior contract: persistence, recovery, configuration, or troubleshooting change. - Update [CONTEXT.md](CONTEXT.md) when the canonical domain language changes; keep it a glossary rather than an architecture guide or progress log. +- Update the [TUI design guide](docs/design-guide.md) when an intentional UI + pattern should become the new project-wide standard. - Update ADR 0002 and the README if serialization stops being normalized or becomes byte-preserving. diff --git a/docs/design-guide.md b/docs/design-guide.md new file mode 100644 index 0000000..ae3eabf --- /dev/null +++ b/docs/design-guide.md @@ -0,0 +1,404 @@ +# 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. +