--- sidebar_position: 17 title: "Extending the Dashboard" description: "Build themes and plugins for the Hermes web dashboard — palettes, typography, layouts, custom tabs, shell slots, page-scoped slots, and backend API routes" --- # Extending the Dashboard The Hermes web dashboard (`hermes dashboard`) is built to be reskinned and extended without forking the codebase. Three layers are exposed: 1. **Themes** — YAML files that repaint the dashboard's palette, typography, layout, and per-component chrome. Drop a file in `~/.hermes/dashboard-themes/`; it appears in the theme switcher. 2. **UI plugins** — a directory with `manifest.json` + a JavaScript bundle that registers a tab, replaces a built-in page, augments one via page-scoped slots, or injects components into named shell slots. 3. **Backend plugins** — a Python file inside that plugin directory that exposes a FastAPI `router`; routes are mounted under `/api/plugins//` and called from the plugin's UI. All three are **drop-in at runtime**: no repo clone, no `npm run build`, no patching the dashboard source. This page is the canonical reference for all three. If you just want to use the dashboard, see [Web Dashboard](./web-dashboard). If you want to reskin the terminal CLI (not the web dashboard), see [Skins & Themes](./skins) — the CLI skin system is unrelated to dashboard themes. :::note How the pieces compose Themes and plugins are independent but synergistic. A theme can stand alone (just a YAML file). A plugin can stand alone (just a tab). Together they let you build a complete visual reskin with custom HUDs — the bundled `strike-freedom-cockpit` demo does exactly that. See [Combined theme + plugin demo](#combined-theme--plugin-demo). ::: --- ## Table of contents - [Themes](#themes) - [Quick start — your first theme](#quick-start--your-first-theme) - [Palette, typography, layout](#palette-typography-layout) - [Layout variants](#layout-variants) - [Theme assets (images as CSS vars)](#theme-assets-images-as-css-vars) - [Component chrome overrides](#component-chrome-overrides) - [Color overrides](#color-overrides) - [Raw `customCSS`](#raw-customcss) - [Built-in themes](#built-in-themes) - [Full theme YAML reference](#full-theme-yaml-reference) - [Plugins](#plugins) - [Quick start — your first plugin](#quick-start--your-first-plugin) - [Directory layout](#directory-layout) - [Manifest reference](#manifest-reference) - [The Plugin SDK](#the-plugin-sdk) - [Shell slots](#shell-slots) - [Replacing built-in pages (`tab.override`)](#replacing-built-in-pages-taboverride) - [Augmenting built-in pages (page-scoped slots)](#augmenting-built-in-pages-page-scoped-slots) - [Slot-only plugins (`tab.hidden`)](#slot-only-plugins-tabhidden) - [Backend API routes](#backend-api-routes) - [Custom CSS per plugin](#custom-css-per-plugin) - [Plugin discovery & reload](#plugin-discovery--reload) - [Combined theme + plugin demo](#combined-theme--plugin-demo) - [API reference](#api-reference) - [Troubleshooting](#troubleshooting) --- ## Themes Themes are YAML files stored in `~/.hermes/dashboard-themes/`. The file name doesn't matter (the theme's `name:` field is what the system uses), but convention is `.yaml`. Every field is optional — missing keys fall back to the built-in `default` theme, so a theme can be as small as one color. ### Quick start — your first theme ```bash mkdir -p ~/.hermes/dashboard-themes ``` ```yaml # ~/.hermes/dashboard-themes/neon.yaml name: neon label: Neon description: Pure magenta on black palette: background: "#000000" midground: "#ff00ff" ``` Refresh the dashboard. Click the palette icon in the header and pick **Neon**. The background goes black, text and accents go magenta, and every derived color (card, border, muted, ring, etc.) is recomputed from that 2-color triplet via `color-mix()` in CSS. That's the whole onboarding: one file, two colors. Everything below is optional refinement. ### Palette, typography, layout These three blocks are the heart of a theme. Each is independent — override one, leave the others. #### Palette (3-layer) The palette is a triplet of color layers plus a warm-glow vignette color and a noise-grain multiplier. The dashboard's design-system cascade derives every shadcn-compatible token (card, popover, muted, border, primary, destructive, ring, etc.) from this triplet via CSS `color-mix()`. Overriding three colors cascades into the whole UI. | Key | Description | |-----|-------------| | `palette.background` | Deepest canvas color — typically near-black. Drives the page background and card fill. | | `palette.midground` | Primary text and accent. Most UI chrome reads this (foreground text, button outlines, focus rings). | | `palette.foreground` | Top-layer highlight. The default theme sets this to white at alpha 0 (invisible); themes that want a bright accent on top can raise its alpha. | | `palette.warmGlow` | `rgba(...)` string used as the vignette color by ``. | | `palette.noiseOpacity` | 0–1.2 multiplier on the grain overlay. Lower = softer, higher = grittier. | Each layer accepts either `{hex: "#RRGGBB", alpha: 0.0–1.0}` or a bare hex string (alpha defaults to 1.0). ```yaml palette: background: hex: "#05091a" alpha: 1.0 midground: "#d8f0ff" # bare hex, alpha = 1.0 foreground: hex: "#ffffff" alpha: 0 # invisible top layer warmGlow: "rgba(255, 199, 55, 0.24)" noiseOpacity: 0.7 ``` #### Typography | Key | Type | Description | |-----|------|-------------| | `fontSans` | string | CSS font-family stack for body copy (applied to `html`, `body`). | | `fontMono` | string | CSS font-family stack for code blocks, ``, `.font-mono` utilities. | | `fontDisplay` | string | Optional heading/display stack. Falls back to `fontSans`. | | `fontUrl` | string | Optional external stylesheet URL. Injected as `` in `` on theme switch. Same URL is never injected twice. Works with Google Fonts, Bunny Fonts, self-hosted `@font-face` sheets — anything linkable. | | `baseSize` | string | Root font size — controls the rem scale. E.g. `"14px"`, `"16px"`. | | `lineHeight` | string | Default line-height. E.g. `"1.5"`, `"1.65"`. | | `letterSpacing` | string | Default letter-spacing. E.g. `"0"`, `"0.01em"`, `"-0.01em"`. | ```yaml typography: fontSans: '"Orbitron", "Eurostile", "Impact", sans-serif' fontMono: '"Share Tech Mono", ui-monospace, monospace' fontDisplay: '"Orbitron", "Eurostile", sans-serif' fontUrl: "https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" baseSize: "14px" lineHeight: "1.5" letterSpacing: "0.04em" ``` #### Layout | Key | Values | Description | |-----|--------|-------------| | `radius` | any CSS length (`"0"`, `"0.25rem"`, `"0.5rem"`, `"1rem"`, ...) | Corner-radius token. Maps to `--radius` and cascades into `--radius-sm/md/lg/xl` — every rounded element shifts together. | | `density` | `compact` \| `comfortable` \| `spacious` | Spacing multiplier applied as the `--spacing-mul` CSS var. `compact = 0.85×`, `comfortable = 1.0×` (default), `spacious = 1.2×`. Scales Tailwind's base spacing, so padding, gap, and space-between utilities all shift proportionally. | ```yaml layout: radius: "0" density: compact ``` ### Layout variants `layoutVariant` picks the overall shell layout. Defaults to `"standard"` when absent. | Variant | Behaviour | |---------|-----------| | `standard` | Single column, 1600px max-width (default). | | `cockpit` | Left sidebar rail (260px) + main content. Populated by plugins via the `sidebar` slot — see [Shell slots](#shell-slots). Without a plugin the rail shows a placeholder. | | `tiled` | Drops the max-width clamp so pages can use the full viewport width. | ```yaml layoutVariant: cockpit ``` The current variant is exposed as `document.documentElement.dataset.layoutVariant`, so raw CSS in `customCSS` can target it via `:root[data-layout-variant="cockpit"] ...`. ### Theme assets (images as CSS vars) Ship artwork URLs with a theme. Each named slot becomes a CSS var (`--theme-asset-`) that the built-in shell and any plugin can read. The `bg` slot is automatically wired into the backdrop; other slots are plugin-facing. ```yaml assets: bg: "https://example.com/hero-bg.jpg" # auto-wired into hero: "/my-images/strike-freedom.png" # for plugin sidebars crest: "/my-images/crest.svg" # for header-left plugins logo: "/my-images/logo.png" sidebar: "/my-images/rail.png" header: "/my-images/header-art.png" custom: scanLines: "/my-images/scanlines.png" # → --theme-asset-custom-scanLines ``` Values accept: - Bare URLs — wrapped in `url(...)` automatically. - Pre-wrapped `url(...)`, `linear-gradient(...)`, `radial-gradient(...)` expressions — used as-is. - `"none"` — explicit opt-out. Every asset is also emitted as `--theme-asset--raw` (the unwrapped URL), in case a plugin needs to pass it to `` instead of `background-image`. Plugins read these with plain CSS or JS: ```javascript // In a plugin slot const hero = getComputedStyle(document.documentElement) .getPropertyValue("--theme-asset-hero").trim(); ``` ### Component chrome overrides `componentStyles` restyles individual shell components without writing CSS selectors. Each bucket's entries become CSS vars (`--component--`) that the shell's shared components read. So `card:` overrides apply to every ``, `header:` to the app bar, etc. ```yaml componentStyles: card: clipPath: "polygon(12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%, 0 12px)" background: "linear-gradient(180deg, rgba(10, 22, 52, 0.85), rgba(5, 9, 26, 0.92))" boxShadow: "inset 0 0 0 1px rgba(64, 200, 255, 0.28)" header: background: "linear-gradient(180deg, rgba(16, 32, 72, 0.95), rgba(5, 9, 26, 0.9))" tab: clipPath: "polygon(6px 0, 100% 0, calc(100% - 6px) 100%, 0 100%)" sidebar: {} backdrop: {} footer: {} progress: {} badge: {} page: {} ``` Supported buckets: `card`, `header`, `footer`, `sidebar`, `tab`, `progress`, `badge`, `backdrop`, `page`. Property names use camelCase (`clipPath`) and are emitted as kebab (`clip-path`). Values are plain CSS strings — anything CSS accepts (`clip-path`, `border-image`, `background`, `box-shadow`, `animation`, ...). ### Color overrides Most themes won't need this — the 3-layer palette derives every shadcn token. Use `colorOverrides` when you want a specific accent the derivation won't produce (a softer destructive red for a pastel theme, a specific success green for a brand). ```yaml colorOverrides: primary: "#ffce3a" primaryForeground: "#05091a" accent: "#3fd3ff" ring: "#3fd3ff" destructive: "#ff3a5e" border: "rgba(64, 200, 255, 0.28)" ``` Supported keys: `card`, `cardForeground`, `popover`, `popoverForeground`, `primary`, `primaryForeground`, `secondary`, `secondaryForeground`, `muted`, `mutedForeground`, `accent`, `accentForeground`, `destructive`, `destructiveForeground`, `success`, `warning`, `border`, `input`, `ring`. Each key maps 1:1 to the `--color-` CSS var (e.g. `primaryForeground` → `--color-primary-foreground`). Any key set here wins over the palette cascade for the active theme only — switching to another theme clears the overrides. ### Raw `customCSS` For selector-level chrome that `componentStyles` can't express — pseudo-elements, animations, media queries, theme-scoped overrides — drop raw CSS into `customCSS`: ```yaml customCSS: | /* Scanline overlay — only visible when cockpit variant is active. */ :root[data-layout-variant="cockpit"] body::before { content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 100; background: repeating-linear-gradient(to bottom, transparent 0px, transparent 2px, rgba(64, 200, 255, 0.035) 3px, rgba(64, 200, 255, 0.035) 4px); mix-blend-mode: screen; } ``` The CSS is injected as a single scoped `