DESIGN.md@desktop ยท diff

git:20260824.874e13a to git:20260825.3be794a

120 added, 0 removed. Audit A to A.

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