DESIGN.md@desktop · git:20261029.6851198 · 2026-10-29 · sha256 50d7f4a11f0ad6d9

DESIGN.md@desktop git:20261029.6851198A

Immutable. This exact content never changes and is served at /api/v1/blob/50d7f4a11f0ad6d9.

# Desktop design conventions

This document records visual and shared-component constraints that should
remain true as the Desktop UI grows. A new feature should follow these
constraints instead of creating a second implementation with similar behavior.

For product-level interaction principles, see
[`UX_GUIDELINES.md`](UX_GUIDELINES.md). For concrete feedback, motion, timing,
recovery, and accessibility defaults, see
[`UX_GUARDRAILS.md`](UX_GUARDRAILS.md).

## Visual direction

SeekMoon uses a quiet, low-chrome interface. "Low chrome" means that borders,
dividers, and decorative frames recede behind the content. A surface should
usually be recognized from a small tonal difference and, when necessary, a
soft ambient shadow rather than from a visible outline.

This is not neumorphism. Components should remain flat and should not use
gradients, inset highlights, or paired light and dark shadows to simulate
physical thickness.

### Surface hierarchy

Use the smallest visual distinction that still makes the structure clear:

1. The canvas uses `--color-bg-canvas`.
2. A card above the canvas uses `--color-bg-surface` and, when it needs a
   persistent boundary, `--shadow-surface`.
3. A compact control inside a card uses `--color-control-surface`. It normally
   needs neither a visible border nor its own shadow.
4. A temporary overlay uses `--color-bg-surface` and `--shadow-overlay` because
   it must remain distinct while crossing other surfaces.

Semantic surfaces follow the same hierarchy. When a status needs a surface,
mix a semantic soft color with the appropriate neutral surface. A status that
already has clear placement and spacing may remain unboxed instead.

### Boundaries

- Prefer flat tonal separation before adding a border.
- Keep a one-pixel transparent border where focus or another state will later
  recolor that edge. This reserves the geometry and prevents layout movement.
- Use `--shadow-surface` for a persistent card and `--shadow-overlay` for a
  temporary surface. Do not invent a new component-specific shadow when one of
  these roles fits.
- Use a visible border only when the boundary itself carries information or
  when adjacent surfaces cannot otherwise remain distinguishable.
- Keep structural separators quiet. They organize regions but should not make
  every region look like a boxed panel.

Avoid using gradients, inset highlights, or hard drop shadows merely to make a
surface look more "finished." Those effects imply physical thickness and do
not belong to this design language.

### Controls and focus

The idle state should feel calm. Focus is the moment when a field boundary is
allowed to become explicit.

- An idle field wrapper uses a transparent border. The input composer is the
  exception described under its reference surface.
- The element marked with `data-focus-owner` owns the one visible focus edge.
- A nested `data-focus-target` does not draw another outline.
- Hover may strengthen the control fill and text, but should not introduce a
  second border or shadow.
- Keyboard focus must remain unmistakable even though idle boundaries are
  quiet.

The detailed ownership contract lives in `styles/README.md` and the shared
focus rule lives in `styles/base.css`.

### Semantic status

Error, warning, and success states should identify themselves without filling
every surrounding surface with status color.

- Use a compact semantic icon as the strongest color signal where possible.
- A terse, unboxed status may use semantic text color. Longer explanatory prose
  normally stays on a text role such as `--color-text-secondary`; exact
  diagnostics may remain semantic when the whole row represents failure.
- Prefer an unboxed status row when placement, spacing, and semantic color
  already make its role clear.
- When a status needs stronger grouping, use its semantic soft color as a
  restrained surface tint.
- Preserve exact diagnostics and inline code; visual quietness must not reduce
  technical precision.
- A durable transcript status stays in transcript order. Presentation must not
  change whether the status is persisted.

### Reference surfaces

Use these existing components as the visual reference before creating a new
surface treatment.

#### Idle input composer

`.composer-inner` is the reference persistent card:

- `border: 1px solid var(--color-border)`
- `background: var(--color-bg-surface)`
- `box-shadow: var(--shadow-surface)`
- `border-radius: var(--radius-lg)`

The composer is the page's standing input target, so its edge stays visible
while idle; the shared focus-owner rule recolors it on focus. The card holds a
single row: the attachment button, the draft, and the send or stop actions,
which stay on the draft's last line as it grows. The controls that shape the
next turn sit below the card as quiet text in `.composer-toolbar`: approval,
worktree, and git state lead; the model and reasoning trail.

#### Transcript error

`.transcript-error-row` is an unboxed durable status inside transcript order:

- no background, border, or shadow
- danger color on the status icon and exact diagnostic text
- tighter row spacing than surfaced cards and prose blocks
- wrapping preserved for long diagnostics and inline code

It should read as transcript status, not as a separate alert system.

### Visual review requirements

Before accepting a new or changed surface, check all of the following:

- Is the visible border necessary, or can tone and an existing shadow define
  the boundary?
- Does the component stay flat without gradients or inset highlights?
- Is its shadow using the correct shared elevation role?
- Does semantic color identify the state without overwhelming the content?
- Does keyboard focus remain clear and have exactly one owner?
- Does the result remain readable in light and dark themes?
- Do long labels, diagnostics, and URLs wrap without horizontal overflow at a
  390px viewport?

## Select controls

Every user-visible single-choice select menu in the Desktop must use
`openseek_desktop/frontend/select_control`. Call
`@select_control.Control::render` directly, or use a package wrapper that
delegates to that method, such as the composer model and reasoning selectors.

Do not add an `@html.select`, a handwritten `<select>`, or a feature-local
button/listbox implementation. Native selects vary across webviews and cannot
share the Desktop's popup styling and interaction behavior. A second custom
implementation would let keyboard, focus, accessibility, and visual behavior
drift between screens.

If a new select needs behavior or a visual form that `Control` cannot express,
extend `select_control` first. Add the required input or `Presentation` variant,
shared styles, interaction behavior, and tests there, then use it from the
feature. Do not bypass the shared control to meet a local deadline.

### Component and state boundary

`Control` is a shared view component. Its public rendering boundary is:

```moonbit
pub fn Control::render(Self) -> @html.Html
```

It deliberately does not return `@rabbita.Val[@html.Html]`. A Rabbita component
with that return type owns or derives an independently updating subtree. A
select's selected value and open state are application state, so they belong to
the caller's model: either the root model or a containing stateful component.
The caller's `Val` causes its view to run again, and that view renders the
shared `Control` with the new values.

The caller owns:

- the options and stable wire values;
- the selected value, disabled state, and whether this menu is open;
- a stable, unique DOM `id`;
- update messages for toggle, dismiss, and value changes; and
- closing or replacing the open menu during application-level transitions.

`select_control` owns:

- the trigger, popup, groups, option rows, and selected indicator;
- the `listbox`/`option` roles and related ARIA attributes;
- label presentation separately from wire values;
- Arrow, Home, End, Escape, and focus behavior, including dismissal when focus
  leaves the control; and
- the shared `Composer` and `Setting` presentations and their styling.

Document-level click-away handling may dispatch the caller's dismiss message,
but it must not duplicate the control's DOM or keyboard behavior.

### Review requirements

A change that introduces or modifies a select must demonstrate that:

- rendered application markup contains no native `<select>` for that control;
- the feature renders `@select_control.Control` directly or through a documented
  wrapper;
- selection and open-menu transitions are tested in the caller that owns the
  state; and
- shared keyboard, focus, accessibility, or presentation changes are tested in
  `frontend/select_control` rather than copied into feature packages.