# Desktop design conventions

This document records frontend 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.

## 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 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 transparent`
- `background: var(--color-bg-surface)`
- `box-shadow: var(--shadow-surface)`
- `border-radius: var(--radius-md)`

Its boundary comes from the surface and ambient shadow. The transparent edge
becomes explicit only when the shared focus-owner rule recolors it.

#### 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.
