Themes and plugins can now pull off arbitrary dashboard reskins (cockpit
HUD, retro terminal, etc.) without touching core code.
Themes gain four new fields:
- layoutVariant: standard | cockpit | tiled — shell layout selector
- assets: {bg, hero, logo, crest, sidebar, header, custom: {...}} —
artwork URLs exposed as --theme-asset-* CSS vars
- customCSS: raw CSS injected as a scoped <style> tag on theme apply
(32 KiB cap, cleaned up on theme switch)
- componentStyles: per-component CSS-var overrides (clipPath,
borderImage, background, boxShadow, ...) for card/header/sidebar/
backdrop/tab/progress/badge/footer/page
Plugin manifests gain three new fields:
- tab.override: replaces a built-in route instead of adding a tab
- tab.hidden: register component + slots without adding a nav entry
- slots: declares shell slots the plugin populates
10 named shell slots: backdrop, header-left/right/banner, sidebar,
pre-main, post-main, footer-left/right, overlay. Plugins register via
window.__HERMES_PLUGINS__.registerSlot(name, slot, Component). A
<PluginSlot> React helper is exported on the plugin SDK.
Ships a full demo at plugins/strike-freedom-cockpit/ — theme YAML +
slot-only plugin that reproduces a Gundam cockpit dashboard: MS-STATUS
sidebar with live telemetry, COMPASS crest in header, notched card
corners via componentStyles, scanline overlay via customCSS, gold/cyan
palette, Orbitron typography.
Validation:
- 15 new tests in test_web_server.py covering every extended field
- tests/hermes_cli/: 2615 passed (3 pre-existing unrelated failures)
- tsc -b --noEmit: clean
- vite build: 418 kB bundle, ~2 kB delta for slots/theme extensions
Co-authored-by: Teknium <p@nousresearch.com>
134 lines
3.4 KiB
TypeScript
134 lines
3.4 KiB
TypeScript
/**
|
|
* Dashboard Plugin SDK + Registry
|
|
*
|
|
* Exposes React, UI components, hooks, and utilities on the window so
|
|
* that plugin bundles can use them without bundling their own copies.
|
|
*
|
|
* Plugins call window.__HERMES_PLUGINS__.register(name, Component)
|
|
* to register their tab component.
|
|
*/
|
|
|
|
import React, {
|
|
useState,
|
|
useEffect,
|
|
useCallback,
|
|
useMemo,
|
|
useRef,
|
|
useContext,
|
|
createContext,
|
|
} from "react";
|
|
import { api, fetchJSON } from "@/lib/api";
|
|
import { cn, timeAgo, isoTimeAgo } from "@/lib/utils";
|
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Select, SelectOption } from "@/components/ui/select";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { useI18n } from "@/i18n";
|
|
import { registerSlot, PluginSlot } from "./slots";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Plugin registry — plugins call register() to add their component.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type RegistryListener = () => void;
|
|
|
|
const _registered: Map<string, React.ComponentType> = new Map();
|
|
const _listeners: Set<RegistryListener> = new Set();
|
|
|
|
function _notify() {
|
|
for (const fn of _listeners) {
|
|
try { fn(); } catch { /* ignore */ }
|
|
}
|
|
}
|
|
|
|
/** Register a plugin component. Called by plugin JS bundles. */
|
|
function registerPlugin(name: string, component: React.ComponentType) {
|
|
_registered.set(name, component);
|
|
_notify();
|
|
}
|
|
|
|
/** Get a registered component by plugin name. */
|
|
export function getPluginComponent(name: string): React.ComponentType | undefined {
|
|
return _registered.get(name);
|
|
}
|
|
|
|
/** Subscribe to registry changes (returns unsubscribe fn). */
|
|
export function onPluginRegistered(fn: RegistryListener): () => void {
|
|
_listeners.add(fn);
|
|
return () => _listeners.delete(fn);
|
|
}
|
|
|
|
/** Get current count of registered plugins. */
|
|
export function getRegisteredCount(): number {
|
|
return _registered.size;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Expose SDK + registry on window
|
|
// ---------------------------------------------------------------------------
|
|
|
|
declare global {
|
|
interface Window {
|
|
__HERMES_PLUGIN_SDK__: unknown;
|
|
__HERMES_PLUGINS__: {
|
|
register: typeof registerPlugin;
|
|
registerSlot: typeof registerSlot;
|
|
};
|
|
}
|
|
}
|
|
|
|
export function exposePluginSDK() {
|
|
window.__HERMES_PLUGINS__ = {
|
|
register: registerPlugin,
|
|
registerSlot,
|
|
};
|
|
|
|
window.__HERMES_PLUGIN_SDK__ = {
|
|
// React core — plugins use these instead of importing react
|
|
React,
|
|
hooks: {
|
|
useState,
|
|
useEffect,
|
|
useCallback,
|
|
useMemo,
|
|
useRef,
|
|
useContext,
|
|
createContext,
|
|
},
|
|
|
|
// Hermes API client
|
|
api,
|
|
// Raw fetchJSON for plugin-specific endpoints
|
|
fetchJSON,
|
|
|
|
// UI components (shadcn/ui primitives)
|
|
components: {
|
|
Card,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardContent,
|
|
Badge,
|
|
Button,
|
|
Input,
|
|
Label,
|
|
Select,
|
|
SelectOption,
|
|
Separator,
|
|
Tabs,
|
|
TabsList,
|
|
TabsTrigger,
|
|
PluginSlot,
|
|
},
|
|
|
|
// Utilities
|
|
utils: { cn, timeAgo, isoTimeAgo },
|
|
|
|
// Hooks
|
|
useI18n,
|
|
};
|
|
}
|