Skip to content

UI Layer

Se.Cherkasov edited this page Aug 23, 2026 · 1 revision

UI layer

English · Русский

One Compose Multiplatform module (composeApp/) draws every platform. The phone UI is common code, not an Android-only branch — which is why desktop tests can drive it.

Structure

composeApp/src/commonMain/kotlin/app/skerry/ui/
  app/          shell state, locals, UI tags, render backend
  design/       the design system: primitives, dialogs, menus, fields, folders
  desktop/      desktop shell: rail, viewport, work bar
  mobile/       phone shell: tabs, sheets, screens
  terminal/ session/ files/ sftp/ remote/ vnc/ rdp/   the working surfaces
  host/ vault/ identity/ known/ trust/ tunnel/ snippet/ runbook/ teams/ share/ ai/ sync/ metrics/ settings/
  i18n/ theme/ nav/ update/ keepalive/
  AppDependencies.kt

AppDependencies is the graph the platform entry point assembles and hands to the root composable (DesktopDesignApp / MobileDesignApp). A subsystem that is not wired up on a platform is null, and the UI shows a placeholder rather than failing.

Design primitives

Screens do not use raw Material components. The catalogue in docs/coding-guidelines.md is normative; the load-bearing entries:

Need Primitive
Any text Txt — never a raw Text() (100+ files)
Any icon Sym (Material Symbols glyph) + DesignFonts
Buttons PrimaryButton, GhostButton, CancelButton, IconBtn, GlyphButton
Small controls Toggle, Badge, Dot, MeterBar, NumberStepper, HoverTooltip
Rows ToggleRow, KeyValueRow, RowNote / NoteBlock
Menus MenuPanel + MenuItem / MenuActionRow — every right-click and "⋮" menu, the text context menu included
Dialogs ConfirmActionDialog, NoticeDialog, ModalScrim, modalBody
A command a dialog quotes CommandQuote (+ ClippedNotice); a command drawn in a row: CommandQuote.CommandLine
Fields FormField + Modifier.fieldName(), ModalTextField, FieldDraft
Folders Folders + FolderSectionHeader, GroupSelectField (desktop) / MobileGroupSelectField
Untrusted text UntrustedText.untrustedLabel, sanitizeServerText, sanitizeServerHost
A question a connection asks PromptQueue
Announcements for a screen reader StatusAnnouncer
Keeping the keyboard on a live widget KeyboardClaim

Two rules behind most of them: one definition per shape (a third hand-rolled copy is a bug), and accessibility comes from the primitive — a caption published by FormField becomes the input's accessible name, so a field cannot end up anonymous.

Tokens and theme

Colours come from D.* design tokens mirrored from the prototype's :root block (docs/design/Skerry Tablet.html), never as hex in a screen. A new shade means a new token, checked against the prototype. Themes: dark, light, and System following the OS; the terminal follows the app theme.

Chrome follows the prototypes 1:1 — the UI is built from them rather than invented.

i18n

Every user-visible string is a resource in composeResources, translated into English, Russian and Simplified Chinese at once. A hardcoded literal fails the deterministic checks.

Two gotchas the code encodes:

  • each locale quotes with its own marks (" " in en and zh, « » in ru — never the straight ");
  • in composeResources the plugin decodes \n, \t, \uXXXX and \\ and nothing else, and substitution is a regex over %N$s / %N$d, not String.format — an Android-style \" or a bare %s is a bug. StringEscapeTest sweeps every string in every locale for all four.

Desktop⇆Android parity

Desktop and mobile share form state and logic (*FormState); only the layout differs. A new form starts as a shared state class and gets two thin views. Parity is checked in review, and the Android compilation of a UI change is a gate stage (:androidApp:compileDebugKotlin).

Controllers

Logic does not live in composables. Controllers (ConnectionController, SessionsController, RemoteDesktopController, SyncCoordinator, RunbookRunner, …) take their dependencies through the constructor — a class that cannot be tested without network, disk or UI is designed wrong. Concurrent controllers ship with tests for cancellation and re-entry.

Testing the real shell

desktopTest/ui/desktop/ShellHarness runs the real UI over a demo graph: runDesktopShell, runMobileShell, runForm, plus onField / onCatalog / onTab / press. Both shells run on the desktop JVM — the mobile UI is common code, so Android parity is checked without an emulator. Navigation is addressed through ui/app/UiTags, because a rail button draws a font ligature and every label is localized.

Clone this wiki locally