SynVirt Frontend Principles
Synced read-only from
/home/synnet/mirrors/synvirt-product/docs/FRONTEND_PRINCIPLES.md. Edit at the source, not here.
SynVirt Frontend Principles
Section titled “SynVirt Frontend Principles”SynVirt’s operator is an IT admin who runs a small-to-medium shop, not a Linux expert. The dashboard is their daily driver. This document defines the experience rules that every pixel of web-ux must honor. Breaking a rule requires an explicit waiver in the PR description.
The operator profile
Section titled “The operator profile”- Reads English (technical English is OK; CLI jargon is not).
- Knows Windows and occasionally macOS. Does not know Linux internals.
- Manages 5–50 VMs across 1–4 hosts.
- Has 2–10 minutes to investigate when something alerts at 2 AM.
- Uses the product on desktop (primary) and mobile (occasional, 2 AM).
- Values predictability over power. Does not enjoy surprises.
Vocabulary translation
Section titled “Vocabulary translation”Never expose raw technical terms in primary UI. Translate:
| Raw term | UI term |
|---|---|
| zpool | Storage pool |
| dataset | Storage folder |
| zvol | Virtual disk |
| bridge / vSwitch | Network |
| VLAN ID | Network tag |
| uplink | Physical link |
| libvirt domain | Virtual machine |
| qemu-guest-agent | Guest tools |
| cloud-init | First-boot setup |
| NUMA node | CPU group |
| ashift | Block size (advanced) |
| IOMMU / SR-IOV | Hardware isolation (advanced) |
Raw terms are allowed in “Advanced” collapsed sections and in tooltips.
The five-second rule
Section titled “The five-second rule”On dashboard load, within 5 seconds the operator must know:
- Are all VMs healthy? (single status line)
- Is storage OK? (single status line)
- Is network OK? (single status line)
- Are there any alerts requiring action?
- When was the last successful backup? (once backups ship)
If the answer to any of these takes more than one glance, the design failed.
Information hierarchy
Section titled “Information hierarchy”Every screen has exactly one primary action. It is the biggest, most colorful button, in the top-right or center-top. Secondary actions are smaller. Tertiary actions live in overflow menus (three-dot icon).
Pages with more than 7 top-level things visible at once need a rethink.
Loading, saving, errors
Section titled “Loading, saving, errors”Loading latency tiers:
- < 100 ms: no indicator
- 100–500 ms: skeleton placeholder
- 500 ms – 3 s: spinner with context text (“Loading your VMs…”)
-
3 s: progress bar, cancelable button
These tiers apply to the FIRST load of a view only. Once a view has data, refreshes follow the stale-while-revalidate discipline (§“Foundation primitives”): the data stays on screen, a subtle indicator marks the refresh, and a failed refresh keeps the stale rows with an error banner — never a spinner over content, never a blank.
Saving:
- Optimistic UI where safe (toggling a light setting).
- Pessimistic with spinner where not (starting a VM, changing network).
- Success confirmation is a toast that auto-dismisses at 4s with a “View details” link for audit.
- Failures show a banner that stays until the user dismisses or retries.
Errors:
- Never display a raw error from libvirt, virsh, or systemctl.
- Map every known error to a human sentence + suggested action.
- Unknown errors: “Something unexpected happened. [Show details] [Report]”. The [Show details] reveals the raw message for the power user.
Error translation lives in the dashboard’s API error map
(crates/web-ux-v2/src/api/errors.ts — the ApiError / ERROR_MAP
surface); legacy web-ux used assets/error-messages.json. Example:
{ "virsh_not_enough_memory": { "user_message": "Not enough free RAM on this host to start the VM.", "suggested_action": "Shut down another VM or add more RAM.", "severity": "error" }}Destructive actions
Section titled “Destructive actions”Destructive = stopping a VM, deleting a VM, wiping a pool, factory reset.
Rules:
- Confirmation modal with the VM/resource name typed by the user (“Type the VM name to confirm deletion: _______”).
- Deleted resources go to Trash for 7 days by default. Trash is visible in Settings. Hard delete requires additional confirmation.
- Undo button on the success toast for 10 seconds after the action.
- Never show two destructive buttons side by side. Always separate with whitespace or other actions between them.
Empty states
Section titled “Empty states”Never show “No data” or an empty table. Always:
- A short explanation of what this view is for.
- A primary CTA to populate the view.
- Optional secondary CTA to learn more.
Example (empty VM list): “No virtual machines yet. Create your first VM to start hosting services. [Create Ubuntu VM] [Custom VM] [Learn more →]”
- Validate on blur, not on keystroke.
- Error message is specific: “Hostname already taken on node-01” rather than “Invalid”.
- Required fields marked with *. Optional fields in a collapsed “More” section when there are more than 5 total.
- Smart defaults fill every field. The operator who clicks through with no changes gets a working VM.
Navigation
Section titled “Navigation”- Persistent top nav: logo (home), VMs, Storage, Network, Settings, user menu (right edge).
- Breadcrumbs on detail pages: VMs › my-vm-01 › Disks.
- Back button in every detail screen (top-left), never relies on browser back only.
- Keyboard:
/focuses search,?opens shortcuts modal,g vgoes to VMs,g sgoes to Storage (future — not MVP).
Mobile
Section titled “Mobile”- Minimum supported viewport: 375×667 (iPhone SE).
- Dashboard home, VM list, VM detail, alerts, logout must be usable.
- Creating/editing resources on mobile is not required; a message “Create new VMs from a larger screen” is acceptable.
Accessibility
Section titled “Accessibility”- All interactive elements reachable by Tab.
- Visible focus ring (Tailwind
focus:ring-2). - Contrast AA minimum (Tailwind’s default colors mostly comply).
- ARIA labels on icon-only buttons.
- Semantic HTML:
<button>,<nav>,<main>, not a soup of<div>. - Reduced motion honored (
prefers-reduced-motion).
Visual language
Section titled “Visual language”- Theme is operator-selectable: light / dark / system (tri-state toggle via
useTheme/ThemeToggle). Default follows the OS (prefers-color-scheme). - Brand primary is
#0566C1(token--color-accent). Cyan#06b6d4is a secondary accent for highlights, charts, and network visualizations — NEVER as the brand. - Palette defined by CSS vars:
--bg-base#0b0f17--bg-elevated#131824--text-primary#e6edf7--text-secondary#8b95a8--color-accent#0566C1 (actions, links — SYNVirt brand)--color-cyan#06b6d4 (highlights, charts, status pills)--ok#10b981--warn#f59e0b--danger#ef4444--muted#5a6478
- Fonts: Hanken Grotesk Variable (display/body) + JetBrains Mono (monospace, IDs/paths/technical values). Self-hosted in the crate (@fontsource-variable), no external CDN.
- Icons: Lucide, inline SVG, no icon fonts.
- Corner radius: 12px cards, 8px inputs, 6px buttons, 4px badges.
- Shadows: subtle, 0 1px 2px for flat, 0 12px 32px for modals.
- Animation: 150–250 ms, easing cubic-bezier(.16,1,.3,1).
- Never use purple-pink gradients. Accent gradient (when used) is the brand
blue
#0566C1on v2, or cyan → blue on the legacy installer/console.
String externalization (i18n-ready)
Section titled “String externalization (i18n-ready)”All UI strings live in the dashboard’s i18n catalog (crates/web-ux-v2/src/i18n/en_us.json; legacy web-ux uses assets/strings/en_us.json) with semantic keys:
{ "nav.vms": "Virtual Machines", "vm.list.empty.title": "No virtual machines yet.", "vm.list.empty.cta_primary": "Create Ubuntu VM", "vm.action.start": "Start", "vm.action.stop": "Shut down", "error.vm.not_found": "This VM no longer exists. It may have been deleted."}Components call t('nav.vms'). When es_MX lands later, a sibling file
gets created and a language switcher toggles between them. Dashboard
does not hardcode a single English string outside this file.
Foundation primitives
Section titled “Foundation primitives”The loading / empty / status vocabulary is a fixed set of shared components — new surfaces compose these, they do not reinvent them. Every view is audited against the triad: what does it show while loading (skeleton), when there is nothing (empty state), and when the backend fails (mapped error + stale data)?
Skeletons — ui/Skeleton.vue + ui/SkeletonTable.vue.
A skeleton matches the FINAL layout: same size, same density, same
column tracks, so the swap to data causes zero layout shift. Gate it on
the first load only (initialLoading). The shimmer stops under
prefers-reduced-motion (and the in-app data-reduced-motion
override) while the placeholder stays visible. Skeleton elements are
aria-hidden; the swapping container carries role="status" + a
human label. No spinner storms: one view renders at most one loading
treatment at a time.
Stale-while-revalidate — composables/useSwr.ts (swrState).
Stores track each fetched collection through swrState().track(fn):
initialLoading (first load, nothing cached — the ONLY skeleton gate),
refreshing (background refresh with data on screen), lastUpdated,
error. Views render cached data immediately and refresh in the
background; the refresh signal is ui/DataFreshnessIndicator.vue
(:refreshing + age ramp) next to the view title. Full-page spinners
on refetch are a defect. A refresh failure keeps the stale rows and
raises a banner; it never blanks the view.
Empty states — ui/EmptyState.vue.
Icon + one-line title + optional description + a primary CTA in the
actions slot (per §“Empty states” — an empty view without a CTA is a
defect). Use size="sm" inside drawers and panel rows.
Status chips — ui/HealthBadge.vue.
THE status pill. Six semantic tones, one visual language app-wide:
| Tone | Meaning | Example labels |
|---|---|---|
healthy |
working as intended | Reachable, Online, App-consistent |
warning |
degraded, honest caveat | Crash-consistent, Stale |
critical |
broken, needs action | Unreachable, Failed |
unknown |
not yet probed | Unknown |
info |
a neutral fact | Immutable · verified, Quiesced |
pending |
waiting by design (dot breathes) | Data plane pending, Queued |
Module code maps its domain states onto these tones + a label; it never
invents a new pill. Free-form categorical tags (not statuses) stay on
ui/Tag.vue.
Motion.
Transitions run 150–250 ms on the --duration-* tokens with the
--easing-* curves (decel for entrances, emphasis for state
changes) — no literal durations in component CSS unless the effect is a
slow ambient loop, and every loop ships a prefers-reduced-motion
(plus [data-reduced-motion]) kill switch. Motion conveys state
(loading, refresh, arrival), never decoration.
Do not
Section titled “Do not”- Do not show JSON payloads to the user.
- Do not show UUIDs. Always resolve to a name.
- Do not show Linux paths (
/var/lib/libvirt/images/...). Show “Storage pool: rpool-fast”. - Do not use modal dialogs for information (“Did you know?”). Modals are for actions only.
- Do not show the browser’s native confirmation (
confirm()). Always use a styled modal. - Do not show spinner-only “Loading…” without context.
- Do not show success messages that require dismissal. Auto-dismiss at 4s.
- Do not show timestamps as ISO 8601. Use relative (“3 minutes ago”) with an absolute tooltip on hover.
- Do not use emojis as UI chrome (allowed in empty states as decoration).
- Do not auto-refresh destructive views. The list of VMs auto-refreshes; the delete modal does not.