Add TUI design guide and update contributing documentation
This commit is contained in:
parent
167a6596b2
commit
3ce7ba8d5c
3 changed files with 411 additions and 0 deletions
|
|
@ -13,6 +13,8 @@ is reachable in the current application.
|
||||||
privileged mode, sudo handling, system-file writes, or backups.
|
privileged mode, sudo handling, system-file writes, or backups.
|
||||||
- Read [ADR 0002](docs/adr/0002-normalize-hosts-serialization.md) before changing
|
- Read [ADR 0002](docs/adr/0002-normalize-hosts-serialization.md) before changing
|
||||||
parsing, serialization, comment placement, or formatting preservation.
|
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
|
## Commands
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
feature. Do not advertise it in user-facing documentation until users can
|
||||||
actually invoke it.
|
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
|
## Domain language and decisions
|
||||||
|
|
||||||
Read [CONTEXT.md](CONTEXT.md) before naming user-visible concepts. Use its
|
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.
|
persistence, recovery, configuration, or troubleshooting change.
|
||||||
- Update [CONTEXT.md](CONTEXT.md) when the canonical domain language changes;
|
- Update [CONTEXT.md](CONTEXT.md) when the canonical domain language changes;
|
||||||
keep it a glossary rather than an architecture guide or progress log.
|
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
|
- Update ADR 0002 and the README if serialization stops being normalized or
|
||||||
becomes byte-preserving.
|
becomes byte-preserving.
|
||||||
|
|
||||||
|
|
|
||||||
404
docs/design-guide.md
Normal file
404
docs/design-guide.md
Normal file
|
|
@ -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.
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue