Consistent styling rules for the Launcher UI. Follow these when building or modifying views and modals.
The app uses a fixed font-size scale. Do not introduce sizes outside this set.
| Size | Usage |
|---|---|
| 11px | Badges, tags, tiny labels |
| 12px | Secondary descriptions, card detail text |
| 13px | Labels, meta text, context menus, field errors, section titles |
| 14px | Body text, buttons, tab labels, inputs, field values |
| 16px | Card names, small modal titles, brand text |
| 18px | View-modal titles |
| 24px | Breadcrumb headings |
| 28px | Hero / welcome titles |
| Element | Size | Notes |
|---|---|---|
| Labels | 13px | color: var(--text-muted) |
| Labels (modal key-value grids) | 13px | font-weight: 600, color: var(--text) — inside modals where surrounding text is already muted, field labels use --text to remain scannable |
| Values (modal key-value grids) | 14px | color: var(--text-muted) — secondary to their bold labels |
| Values / body text | 14px | color: var(--text) |
| Section titles | 13px | uppercase, font-weight: 600, color: var(--text-muted) (in views) or color: var(--text) (in modals) |
| List rows | 13px | |
| Badges / tags | 11px | font-weight: 600, uppercase |
| Buttons | 14px | |
| Inputs / selects | 14px | |
| Card names | 16px | font-weight: 600 |
| Instance name | 16px | font-weight: 600 |
| Modal title (dialog) | 16px | font-weight: 600 |
| Modal title (view) | 18px | font-weight: 600 |
| Breadcrumb | 24px | font-weight: 600 |
| Welcome / hero title | 28px | font-weight: 700 |
| Token | Value | Usage |
|---|---|---|
| badge | 3px | Badges, small tags |
| sm | 6px | Buttons, inputs, progress bars, sidebar items, terminal |
| md | 8px | Cards, panels, detail fields, context menus, banners |
| lg | 12px | Modals (view-modal and dialog), variant-card icons |
| circle | 50% | Circular indicators (status dots, step indicators, avatar circles) |
Do not use border-radius values outside this set (no 9px, 10px, etc.).
Use only values from this set for padding, margins, and gaps:
2 / 4 / 6 / 8 / 10 / 12 / 16 / 20 / 24 / 28 / 32 / 40 / 80
| Context | Value |
|---|---|
| Badge padding | 1px 6px |
| Button padding | 7px 16px |
| Header button padding | 6px 16px |
| Card padding (instance) | 14px 16px |
| Card padding (dashboard) | 16px 20px |
| Modal body padding | 20px |
| Modal dialog padding | 24px |
| Content area padding | 24px 28px |
| Section margin-bottom | 16px |
| Field gap within section | 10px |
| Button group gap | 8px |
| List item gap | 10px |
All themes must define these tokens:
| Token | Purpose |
|---|---|
--bg |
Page and modal backgrounds |
--surface |
Raised cards/elements on top of --bg |
--border / --border-hover |
Borders |
--text / --text-muted / --text-faint |
Text hierarchy |
--accent / --accent-hover |
Interactive highlights, selected states |
--danger |
Destructive actions, errors |
--warning |
Caution states |
--success |
Positive confirmations |
--info |
Informational highlights (blue) |
--terminal-bg |
Terminal/console backgrounds |
--overlay-bg |
Modal overlay |
- No hardcoded hex values — always use
var(--token). - No inline fallbacks — write
var(--info), notvar(--info, #58a6ff). Every token is defined in every theme. - Semantic colors (
--danger,--warning,--success,--info) are used for text color, badge color, and tinted backgrounds viacolor-mix.
Scrollable or nested list containers use a recessed look that contrasts with the modal/page background:
.recessed-list {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px;
}Use the single canonical class recessed-list (defined in main.css). Do not create component-specific variants (no ls-recessed-list, etc.).
Items inside use --bg backgrounds for the inset effect.
When a searchable list has a filter input, place the <input> inside the recessed container (class recessed-search) so the search and results read as one cohesive unit.
- Use
view-modal-contentclass frommain.css(max-width: 900px). Do not override with narrower widths. - Body:
view-modal-body→view-scroll(scrollable) →view-bottom(sticky footer).
- Use
modal-overlay→modal-boxpattern. modal-boxusesborder-radius: 12px.- Title separator:
.modal-titlehasborder-bottom: 1px solid var(--border)withpadding-bottom: 12pxandmargin-bottom: 12px. - Actions separator:
.modal-actionshasborder-top: 1px solid var(--border)withpadding-top: 12px.
- Overlay uses
mousedown+clickdismiss handling (prevents text-select-then-release closing). - Escape key:
Escapekeydown listener ondocumentcloses the modal by default.- To make a modal non-escapable (e.g., consent/agreement dialogs), omit the Escape listener or guard it with a condition.
defineExpose({ open })for parent activation via template ref.
- Title shows a triangle indicator via CSS
::before:▸(collapsed, rotates 90° when expanded). - Title element gets
cursor: pointer; user-select: none. - Use class
collapsibleon section titles (e.g.,detail-section-title collapsible).
The app sets user-select: none globally on body. All data value elements must opt back in with user-select: text so users can select and copy them. This applies to:
- Field values (versions, names, paths)
- Diff lines
- Terminal output
- Error messages
Labels, buttons, section titles, and other chrome remain non-selectable.
Base style: font-size: 11px; font-weight: 600; padding: 1px 6px; border-radius: 3px.
| Variant | Text color | Background | Use |
|---|---|---|---|
| Semantic | A semantic color (--success, --warning, --info, --danger, --text-muted) |
var(--bg) |
Category/trigger labels (BOOT, MANUAL, PRE-UPDATE…) |
| Tinted | A semantic color | color-mix(in srgb, <color> 12%, transparent) |
Diff summaries with add/remove/change meaning (+3 nodes, −2 pkgs) |
| Neutral | var(--text-muted) |
var(--bg) |
Informational chips without semantic weight |
Badge backgrounds must contrast with their parent: on --surface cards use background: var(--bg), on --bg rows (inside recessed lists) use background: var(--surface).
- Group with
display: flex; gap: 8px. - Consistent padding (
6px 16px),font-size: 13px,border-radius: 6px.
- No inline styles — use classes or scoped CSS. If a style is needed in only one place, create a scoped class.
- No
!important— fix specificity with more specific selectors instead. - No hex fallbacks — use
var(--token)without fallback values. - No ad-hoc sizes — use only values from the font-size, border-radius, and spacing scales above.
When spawning child processes (git, python, uv, etc.), always capture stderr and include it in any error message surfaced to the user. A bare exit code like "failed with exit code 1" is never acceptable — the user must see the underlying error.
-
Use
'close', not'exit'— resolve promises on the'close'event, not'exit'. Node.js can fire'exit'before all stdio data has been received, causing empty output buffers:// ✗ BAD — data events may not have fired yet proc.on('exit', (code) => resolve(code ?? 1)) // ✓ GOOD — all stdio streams have ended proc.on('close', (code) => resolve(code ?? 1))
-
Capture both stdout and stderr into buffers while still streaming via
sendOutput. Many tools (Python scripts, git) write errors to stdout, not stderr:let stdoutBuf = '' let stderrBuf = '' proc.stdout.on('data', (chunk: Buffer) => { const text = chunk.toString('utf-8') stdoutBuf += text sendOutput(text) }) proc.stderr.on('data', (chunk: Buffer) => { const text = chunk.toString('utf-8') stderrBuf += text sendOutput(text) })
-
Include output in failure messages — prefer stderr, fall back to stdout, take the last 20 lines:
if (exitCode !== 0) { const detail = (stderrBuf || stdoutBuf).trim().split('\n').slice(-20).join('\n') const message = detail ? `${t('source.updateFailed', { code: exitCode })}\n\n${detail}` : t('source.updateFailed', { code: exitCode }) return { ok: false, message } }
-
For reusable helpers (e.g.
gitClone,gitFetchAndCheckout), return aProcessResultwith bothexitCodeandstderrso callers can format the error:interface ProcessResult { exitCode: number; stderr: string }
This applies to all spawned processes: update scripts, git operations, pip/uv installs, snapshot restores, etc.