Immutable. This exact content is served forever at /api/v1/blob/7dac1551594a7ce1.
# 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 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.