UI Development Guide
This project uses vanilla TypeScript DOM manipulation — no framework. All UI lives in src/renderer/. Follow the patterns and reuse the components documented below.
Custom Dropdown / Select
Never use native <select>. Always use the custom select component:
import { createCustomSelect } from './components/custom-select';
const select = createCustomSelect('my-select', [
{ value: 'a', label: 'Option A' },
{ value: 'b', label: 'Option B' },
{ value: 'c', label: 'Disabled', disabled: true },
], 'a'); // default value
// select.getValue() — get current value
// select.destroy() — cleanup
- File:
src/renderer/components/custom-select.ts - CSS classes:
.custom-select,.custom-select-trigger,.custom-select-dropdown,.custom-select-item - Supports keyboard navigation (Arrow keys, Enter, Escape, Tab)
Modals
Use showModal() for generic modals with form fields:
import { showModal, closeModal, setModalError } from './components/modal';
showModal('My Title', [
{ id: 'name', label: 'Name', type: 'text', placeholder: 'Enter name' },
{ id: 'option', label: 'Option', type: 'select', options: [...] },
{ id: 'enabled', label: 'Enable feature', type: 'checkbox' },
], (values) => {
// values is Record<string, string>
if (!values.name) {
setModalError('name', 'Name is required');
return;
}
// ... handle confirm
closeModal();
});
- File:
src/renderer/components/modal.ts - Exports:
showModal(),closeModal(),setModalError() - Supports field types:
text,checkbox,select(uses custom select internally) - Supports field buttons (e.g., a "Browse" button next to a text input)
- Keyboard: Enter to confirm, Escape to cancel
For specialized modals (complex layout, multi-pane, unique behavior), create a dedicated file in src/renderer/components/ following the existing pattern (e.g., preferences-modal.ts, usage-modal.ts).
Alert Banners
For in-context alerts shown above the terminal:
import { showAlertBanner, removeAlertBanner } from './components/alert-banner';
showAlertBanner({
icon: '⚠️',
message: 'Something happened',
ctaLabel: 'Fix it',
onCta: () => { /* handle action */ },
dismissLabel: 'Dismiss',
onDismiss: () => { removeAlertBanner(); },
});
- File:
src/renderer/components/alert-banner.ts - CSS classes:
.insight-alert,.insight-alert-icon,.insight-alert-message,.insight-alert-cta,.insight-alert-dismiss - Use
.insight-alert-infovariant for informational (blue) alerts
Buttons
Use existing CSS classes — do not create new button styles:
| Class | Use for |
|---|---|
.btn-primary |
Canonical primary/CTA button (accent fill, --accent-dim hover, --radius-md). Use for every solid-accent action. Defined in base.css. |
.btn-secondary |
Canonical secondary button (bordered, neutral --bg-primary fill, --bg-hover hover, same --radius-md + size as primary, normal weight). Use for Cancel and every neutral action. Add .danger for destructive (red). Defined in base.css. |
.btn-sm / .btn-xs |
Compact size modifiers — pair with .btn-primary or .btn-secondary. btn-sm for dense card/toolbar rows; btn-xs for tight github rows. Single-class so a context's geometry class still wins. |
.icon-btn |
Small 26×26px icon buttons (tab bar, sidebar actions) |
.modal-field-btn |
Inline button next to a modal field (e.g., "Browse") |
.config-section-add-btn |
Add button in config sections |
A modal footer is .btn-secondary (Cancel) next to .btn-primary (Confirm) — they share size and corner radius so the pair lines up.
For split buttons (main + chevron), add .btn-primary/.btn-secondary plus the context's geometry-only class (e.g. .widget-github-fix-main, .team-card-chat-dropdown) which is defined later in source order so it correctly overrides the corner-radius/padding without redefining the fill. The same applies to structural-only classes kept on consolidated buttons (.team-card-btn, .widget-team-card-btn, .project-tab-toolbar-btn for its .active state, .share-btn for its .hidden toggle).
Badges
| Class | Use for |
|---|---|
.scope-badge.user / .scope-badge.project |
Scope indicators |
.readiness-badge |
Status badges |
.git-file-badge |
Git status letter indicators |
.file-viewer-area-badge |
Git area badges (staged, working, etc.) |
CSS Theming
Never hardcode colors. Always use CSS variables from src/renderer/styles/base.css:
/* Backgrounds */
var(--bg-primary) /* #000000 — main background */
var(--bg-secondary) /* #0a0a0a — secondary background */
var(--bg-tertiary) /* #1a1a1a — tertiary / elevated surfaces */
var(--bg-hover) /* #222222 — hover state */
/* Text */
var(--text-primary) /* #e0e0e0 */
var(--text-secondary) /* #a0a0b0 */
var(--text-muted) /* #606070 */
/* Accents & borders */
var(--accent) /* #e94560 — primary accent (red) */
var(--accent-dim) /* #c73e55 */
var(--border) /* #333333 */
var(--bookmark) /* #e8a317 */
Semantic status colors (these are not CSS variables — use the hex values directly):
- Working/Active:
var(--accent)with pulse animation - Waiting:
#f4b400(yellow) - Completed/Success:
#34a853(green) - Input:
#e67e22(orange) - Info:
#4285f4(blue) - Idle:
var(--text-muted)
Styling Conventions
- Class naming:
.component-childpattern (e.g.,.modal-field,.tab-status) - State modifiers:
.active,.disabled,.hidden,.focused(e.g.,.tab-item.active) - Border radius:
4pxstandard,2-3pxsmall,8pxrounded/pills,50%circular - Transitions:
0.15sfor hover/focus states - Font: System sans-serif for UI, monospace (
JetBrains Mono,Fira Code, etc.) for code/terminal - Font sizes: 9-13px for UI elements, 13px base
- Scrollbars: Use webkit custom scrollbar (6px width,
var(--border)thumb, 3px radius)
Component Architecture Patterns
Factory functions
Components use factory functions that return an instance object:
export function createMyComponent(id: string, options: Options): MyComponentInstance {
const el = document.createElement('div');
// ... build DOM ...
return {
element: el,
getValue() { /* ... */ },
destroy() { /* cleanup listeners, remove DOM */ },
};
}
State subscriptions
Use appState event emitter for reactive updates:
import { appState } from '../state';
appState.on('session-changed', (sessionId) => { /* update UI */ });
Cleanup
Always provide a destroy() method that removes event listeners and DOM nodes. This prevents memory leaks when sessions/tabs are closed.
DOM creation
- Prefer
document.createElement()+ property assignment overinnerHTML - When using
innerHTML, always escape user content withesc()fromsrc/renderer/components/dom-utils.ts - Use
classList.toggle()/classList.add()/classList.remove()for conditional classes - Use
element.dataset.*for data attributes
Text selection preservation
Components that re-render periodically (timers, event-driven updates) must check for active text selection before wiping the DOM. Destroying DOM nodes while the user is selecting text clears their selection. Guard re-renders like this:
const sel = window.getSelection();
if (sel && sel.rangeCount > 0 && !sel.isCollapsed && container.contains(sel.anchorNode)) {
return; // skip render — user is selecting text
}
Also ensure clickable containers that hold selectable text use stopPropagation() on the text element to prevent click-to-select from triggering the parent's click handler, and set user-select: text; cursor: text; in CSS.
CSS File Organization
Add styles to the appropriate existing CSS file — do not create new CSS files unless introducing a wholly new component area:
| File | Contents |
|---|---|
styles/base.css |
CSS variables, resets, global styles |
styles/modals.css |
Modal, custom select, config sections, path autocomplete |
styles/tabs.css |
Tab bar, icon buttons, context menus |
styles/sidebar.css |
Sidebar, project list, update banner |
styles/terminal.css |
Terminal pane, status bar, exit overlay |
styles/alerts.css |
Alert banners, insight alerts, readiness badges |
styles/search.css |
Search bar, toggle buttons, match highlighting |
styles/dialogs.css |
Help dialog |
styles/preferences.css |
Preferences modal, sections |
styles/session-history.css |
Session history list |
styles/file-viewer.css |
File viewer, diff display |
styles/git-panel.css |
Git panel, worktree selector |
styles/mcp-inspector.css |
MCP inspector pane |
styles/session-inspector.css |
Session inspector panel |