Расширение панели управления
Веб-панель VibeOS (vibeos dashboard) спроектирована так, чтобы её можно было перекрашивать и расширять без форка кодовой базы. Доступны три уровня:
- Темы — YAML-файлы, которые перекрашивают палитру, типографику, макет и оформление отдельных компонентов панели. Положите файл в
~/.vibeos/dashboard-themes/— он появится в переключателе тем. - UI-плагины — каталог с
manifest.jsonи JavaScript-бандлом, который регистрирует вкладку, заменяет встроенную страницу, дополняет её через слоты на уровне страницы или внедряет компоненты в именованные слоты оболочки. - Серверные плагины — Python-файл внутри того же каталога плагина, который предоставляет
routerFastAPI; маршруты монтируются по пути/api/plugins/<name>/и вызываются из UI плагина.
Все три уровня подключаются на лету: не нужно клонировать репозиторий, запускать npm run build или патчить исходники панели. Эта страница — канонический справочник по всем трём уровням.
Если вы просто хотите использовать панель, смотрите Веб-панель. Если хотите перекрасить терминальный CLI (не веб-панель), смотрите Скины и темы — система скинов CLI не связана с темами панели.
Темы и плагины независимы, но синергичны. Тема может существовать сама по себе (просто YAML-файл). Плагин может существовать сам по себе (просто вкладка). Вместе они позволяют создать полную визуальную переработку с пользовательскими HUD — демо strike-freedom-cockpit (находится в сопутствующем репозитории vibeos-example-plugins — шаги по установке см. в разделе Комбинированное демо темы + плагина) делает именно это.
Содержание
- Темы
- Плагины
- Быстрый старт — ваш первый плагин
- Структура каталога
- Справочник манифеста
- SDK плагина
- Слоты оболочки
- Замена встроенных страниц (
tab.override) - Дополнение встроенных страниц (слоты на уровне страниц)
- Плагины только со слотами (
tab.hidden) - Серверные API-маршруты
- Пользовательский CSS для плагина
- Обнаружение и перезагрузка плагинов
- Комбинированное демо темы + плагина
- Справочник API
- Устранение неполадок
Темы
Темы — это YAML-файлы, хранящиеся в ~/.vibeos/dashboard-themes/. Имя файла не имеет значения (система использует поле name: темы), но по соглашению используется <name>.yaml. Каждое поле необязательно — отсутствующие ключи возвращаются к встроенной теме default, поэтому тема может состоять всего из одного цвета.
Быстрый старт — ваша первая тема
mkdir -p ~/.vibeos/dashboard-themes
# ~/.vibeos/dashboard-themes/neon.yaml
name: neon
label: Neon
description: Чистый маджента на чёрном
palette:
background: "#000000"
midground: "#ff00ff"
Обновите панель. Нажмите на иконку палитры в заголовке и выберите Neon. Фон станет чёрным, текст и акценты — маджентовыми, а все производные цвета (карточка, граница, приглушённый, кольцо и т.д.) будут пересчитаны из этой пары цветов с помощью color-mix() в CSS.
Это и есть весь процесс знакомства: один файл, два цвета. Всё, что ниже — необязательные уточнения.
Палитра, типографика, макет
Эти три блока — сердце темы. Каждый независим — переопределите один, остальные останутся без изменений.
Палитра (3 слоя)
Палитра — это триплет цветовых слоёв плюс цвет тёплого свечения виньетки и множитель зернистости шума. Каскад дизайн-системы панели выводит каждый совместимый с shadcn токен (карточка, всплывающее окно, приглушённый, граница, основной, разрушительный, кольцо и т.д.) из этого триплета с помощью CSS color-mix(). Переопределение трёх цветов каскадно распространяется на весь интерфейс.
| Ключ | Описание |
|---|---|
palette.background | Самый глубокий цвет холста — обычно почти чёрный. Определяет фон страницы и заливку карточек. |
palette.midground | Основной текст и акцент. Большинство элементов интерфейса считывают это (цвет текста на переднем плане, контуры кнопок, фокусные кольца). |
palette.foreground | Выделение верхнего слоя. Тема по умолчанию устанавливает его на белый с альфой 0 (невидимый); темы, которые хотят иметь яркий акцент сверху, могут увеличить его альфу. |
palette.warmGlow | Строка rgba(...), используемая как цвет виньетки компонентом <Backdrop />. |
palette.noiseOpacity | Множитель 0–1.2 для наложения зернистости. Меньше = мягче, выше = зернистее. |
Каждый слой принимает либо {hex: "#RRGGBB", alpha: 0.0–1.0}, либо просто шестнадцатеричную строку (альфа по умолчанию 1.0).
palette:
background:
hex: "#05091a"
alpha: 1.0
midground: "#d8f0ff" # просто hex, alpha = 1.0
foreground:
hex: "#ffffff"
alpha: 0 # невидимый верхний слой
warmGlow: "rgba(255, 199, 55, 0.24)"
noiseOpacity: 0.7
Типографика
| Ключ | Тип | Описание |
|---|---|---|
fontSans | строка | Стек CSS font-family для основного текста (применяется к html, body). |
fontMono | строка | Стек CSS font-family для блоков кода, <code>, утилит .font-mono. |
fontDisplay | строка | Необязательный стек для заголовков/отображения. Возвращается к fontSans. |
fontUrl | строка | Необязательный URL внешней таблицы стилей. Внедряется как <link rel="stylesheet"> в <head> при переключении темы. Один и тот же URL никогда не внедряется дважды. Работает с Google Fonts, Bunny Fonts, самостоятельно размещёнными таблицами @font-face` — с любым, на что можно дать ссылку. |
baseSize | строка | Корневой размер шрифта — управляет шкалой rem. Например, "14px", "16px". |
lineHeight | строка | Межстрочный интервал по умолчанию. Например, "1.5", "1.65". |
letterSpacing | строка | Межбуквенный интервал по умолчанию. Например, "0", "0.01em", "-0.01em". |
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"
Изменение шрифта из интерфейса (без YAML)
В переключателе тем в заголовке панели есть раздел Шрифт под списком тем. Выберите любой шрифт — он переопределит основной шрифт активной темы. Выбор независим от темы и сохраняется при переключении тем (хранится в config.yaml в поле dashboard.font). Выберите По умолчанию темы, чтобы сбросить переопределение и вернуться к собственному fontSans активной темы.
Переключатель предлагает курируемый каталог (системные стеки плюс набор семейств Google Fonts для sans/serif/mono). Он намеренно не принимает произвольный URL шрифта — таблица стилей шрифта внедряется как <link>, поэтому каталог сохраняет фиксированные источники внедрения. Для полностью пользовательского шрифта укажите fontSans+fontUrlв YAML темы, как показано выше.fontMono` темы (блоки кода, терминал) всегда остаётся нетронутым переопределением из интерфейса.
Макет
| Ключ | Значения | Описание |
|---|---|---|
radius | любая CSS-длина ("0", "0.25rem", "0.5rem", "1rem", ...) | Токен радиуса углов. Отображается на --radius и каскадно распространяется на --radius-sm/md/lg/xl — все скруглённые элементы изменяются синхронно. |
density | compact | comfortable | spacious | Множитель интервалов, применяемый как CSS-переменная --spacing-mul. compact = 0.85×, comfortable = 1.0× (по умолчанию), spacious = 1.2×. Масштабирует базовые интервалы Tailwind, поэтому утилиты padding, gap и space-between изменяются пропорционально. |
layout:
radius: "0"
density: compact
Варианты макета
layoutVariant выбирает общий макет оболочки. По умолчанию "standard", если отсутствует.
| Вариант | Поведение |
|---|---|
standard | Одна колонка, максимальная ширина 1600px (по умолчанию). |
cockpit | Левая боковая панель (260px) + основной контент. Заполняется плагинами через слот sidebar — см. Слоты оболочки. Без плагина панель показывает заполнитель. |
tiled | Убирает ограничение максимальной ширины, чтобы страницы могли использовать всю ширину окна просмотра. |
layoutVariant: cockpit
Текущий вариант доступен как document.documentElement.dataset.layoutVariant, поэтому сырой CSS в customCSS может нацеливаться на него через :root[data-layout-variant="cockpit"] ....
Ресурсы темы (изображения как CSS-переменные)
Добавляйте URL-адреса изображений в тему. Каждый именованный слот становится CSS-переменной (--theme-asset-<name>), которую могут читать встроенная оболочка и любой плагин. Слот bg` автоматически подключается к фону; остальные слоты предназначены для плагинов.
assets:
bg: "https://example.com/hero-bg.jpg" # автоматически подключается к <Backdrop />
hero: "/my-images/strike-freedom.png" # для боковых панелей плагинов
crest: "/my-images/crest.svg" # для плагинов слева в заголовке
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
Значения принимают:
- Простые URL — автоматически оборачиваются в
url(...). - Предварительно обёрнутые выражения
url(...),linear-gradient(...),radial-gradient(...)— используются как есть. "none"— явный отказ.
Каждый ресурс также выводится как --theme-asset-<name>-raw (нераспакованный URL) на случай, если плагину нужно передать его в <img src> вместо background-image.
Плагины читают их с помощью обычного CSS или JS:
// В слоте плагина
const hero = getComputedStyle(document.documentElement)
.getPropertyValue("--theme-asset-hero").trim();
Переопределение оформления компонентов
componentStyles переопределяет стили отдельных компонентов оболочки без написания CSS-селекторов. Записи каждого блока становятся CSS-переменными (--component-<bucket>-<kebab-property>), которые считывают общие компоненты оболочки. Таким образом, переопределения card:применяются к каждому <Card>,header:` — к панели приложения и т.д.
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: {}
Поддерживаемые блоки: card, header, footer, sidebar, tab, progress, badge, backdrop, page.
Имена свойств используют camelCase (clipPath) и выводятся в kebab-case (clip-path). Значения — обычные CSS-строки — всё, что принимает CSS (clip-path, border-image, background, box-shadow, animation, ...).
Переопределение цветов
Большинству тем это не понадобится — 3-слойная палитра выводит каждый токен shadcn. Используйте colorOverrides, когда вам нужен определённый акцент, который не даёт производная схема (более мягкий разрушительный красный для пастельной темы, определённый зелёный успеха для бренда).
colorOverrides:
primary: "#ffce3a"
primaryForeground: "#05091a"
accent: "#3fd3ff"
ring: "#3fd3ff"
destructive: "#ff3a5e"
border: "rgba(64, 200, 255, 0.28)"
Поддерживаемые ключи: card, cardForeground, popover, popoverForeground, primary, primaryForeground, secondary, secondaryForeground, muted, mutedForeground, accent, accentForeground, destructive, destructiveForeground, success, warning, border, input, ring.
Каждый ключ отображается 1:1 на CSS-переменную --color-<kebab> (например, primaryForeground→--color-primary-foreground`). Любой ключ, установленный здесь, имеет приоритет над каскадом палитры только для активной темы — переключение на другую тему очищает переопределения.
Сырой customCSS
Для оформления на уровне селекторов, которое нельзя выразить через componentStyles — псевдоэлементы, анимации, медиа-запросы, переопределения в рамках темы — поместите сырой CSS в customCSS:
customCSS: |
/* Наложение строк развёртки — видно только когда активен вариант cockpit. */
: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;
}
CSS внедряется как один изолированный тег <style data-vibeos-theme-css> при применении темы и удаляется при переключении темы. Ограничение 32 КБ на тему.
Встроенные темы
Каждая встроенная тема поставляется со своей палитрой, типографикой и макетом — переключение вызывает видимые изменения, выходящие за рамки только цвета.
| Тема | Палитра | Типографика | Макет |
|---|---|---|---|
VibeOS Teal (default) | Тёмный бирюзовый + кремовый | Системный стек, 15px | Радиус 0.5rem, comfortable |
VibeOS Teal (Large) (default-large) | То же, что по умолчанию | Системный стек, 18px, межстрочный 1.65 | Радиус 0.5rem, spacious |
Midnight (midnight) | Глубокий сине-фиолетовый | Inter + JetBrains Mono, 14px | Радиус 0.75rem, comfortable |
Ember (ember) | Тёплый малиновый + бронзовый | Spectral (с засечками) + IBM Plex Mono, 15px | Радиус 0.25rem, comfortable |
Mono (mono) | Оттенки серого | IBM Plex Sans + IBM Plex Mono, 13px | Радиус 0, compact |
Cyberpunk (cyberpunk) | Неоново-зелёный на чёрном | Share Tech Mono везде, 14px | Радиус 0, compact |
Rosé (rose) | Розовый + слоновая кость | Fraunces (с засечками) + DM Mono, 16px | Радиус 1rem, spacious |
Темы, которые ссылаются на Google Fonts (все, кроме VibeOS Teal), загружают таблицу стилей по требованию — при первом переключении на них в <head> внедряется тег <link>`.
Полный справочник YAML темы
Все настройки в одном файле — скопируйте и обрежьте то, что не нужно:
# ~/.vibeos/dashboard-themes/ocean.yaml
name: ocean
label: Ocean Deep
description: Глубокие морские синие с коралловыми акцентами
# 3-слойная палитра (принимает {hex, alpha} или просто hex)
palette:
background:
hex: "#0a1628"
alpha: 1.0
midground:
hex: "#a8d0ff"
alpha: 1.0
foreground:
hex: "#ffffff"
alpha: 0.0
warmGlow: "rgba(255, 107, 107, 0.35)"
noiseOpacity: 0.7
typography:
fontSans: "Poppins, system-ui, sans-serif"
fontMono: "Fira Code, ui-monospace, monospace"
fontDisplay: "Poppins, system-ui, sans-serif" # необязательно
fontUrl: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap"
baseSize: "15px"
lineHeight: "1.6"
letterSpacing: "-0.003em"
layout:
radius: "0.75rem"
density: comfortable
layoutVariant: standard # standard | cockpit | tiled
assets:
bg: "https://example.com/ocean-bg.jpg"
hero: "/my-images/kraken.png"
crest: "/my-images/anchor.svg"
logo: "/my-images/logo.png"
custom:
pattern: "/my-images/waves.svg"
componentStyles:
card:
boxShadow: "inset 0 0 0 1px rgba(168, 208, 255, 0.18)"
header:
background: "linear-gradient(180deg, rgba(10, 22, 40, 0.95), rgba(5, 9, 26, 0.9))"
colorOverrides:
destructive: "#ff6b6b"
ring: "#ff6b6b"
customCSS: |
/* Любые дополнительные корректировки на уровне селекторов */
Обновите панель после создания файла. Переключайте темы в реальном времени из панели заголовка — нажмите на иконку палитры. Выбор сохраняется в config.yaml в поле dashboard.theme и восстанавливается при перезагрузке.
Плагины
Плагин панели — это каталог с manifest.json, предварительно собранным JS-бандлом и, опционально, CSS-файлом и Python-файлом с маршрутами FastAPI. Плагины располагаются рядом с другими плагинами VibeOS в ~/.vibeos/plugins/<name>/ — расширение панели находится в подпапке dashboard/ внутри этого каталога плагина, поэтому один плагин может расширять и CLI/шлюз, и панель из одной установки.
Плагины не включают React или UI-компоненты. Они используют SDK плагина, доступный на window.__VIBEOS_PLUGIN_SDK__. Это позволяет сделать бандлы плагинов крошечными (обычно несколько КБ) и избежать конфликтов версий.
Быстрый старт — ваш первый плагин
Создайте структуру каталога:
mkdir -p ~/.vibeos/plugins/my-plugin/dashboard/dist
Напишите манифест:
// ~/.vibeos/plugins/my-plugin/dashboard/manifest.json
{
"name": "my-plugin",
"label": "Мой плагин",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills"
},
"entry": "dist/index.js"
}
Напишите JS-бандл (простой IIFE — шаг сборки не нужен):
// ~/.vibeos/plugins/my-plugin/dashboard/dist/index.js
(function () {
"use strict";
const SDK = window.__VIBEOS_PLUGIN_SDK__;
const { React } = SDK;
const { Card, CardHeader, CardTitle, CardContent } = SDK.components;
function MyPage() {
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "Мой плагин"),
),
React.createElement(CardContent, null,
React.createElement("p", { className: "text-sm text-muted-foreground" },
"Привет от моей пользовательской вкладки панели.",
),
),
);
}
window.__VIBEOS_PLUGINS__.register("my-plugin", MyPage);
})();
Обновите панель — ваша вкладка появится в панели навигации после Skills.
Если вы предпочитаете JSX, используйте любой сборщик (esbuild, Vite, rollup) с React в качестве внешней зависимости и выводом IIFE. Единственное жёсткое требование — чтобы конечный файл был одним JS-файлом, загружаемым через <script>. React никогда не включается в бандл; он берётся из SDK.React`.
Структура каталога
~/.vibeos/plugins/my-plugin/
├── plugin.yaml # необязательно — существующий манифест плагина CLI/шлюза
├── __init__.py # необязательно — существующие хуки CLI/шлюза
└── dashboard/ # расширение панели
├── manifest.json # обязательно — конфигурация вкладки, иконка, точка входа
├── dist/
│ ├── index.js # обязательно — предварительно собранный JS-бандл (IIFE)
│ └── style.css # необязательно — пользовательский CSS
└── plugin_api.py # необязательно — серверные API-маршруты (FastAPI)
Один каталог плагина может содержать три ортогональных расширения:
plugin.yaml+__init__.py— плагин CLI/шлюза (см. страницу плагинов).dashboard/manifest.json+dashboard/dist/index.js— UI-плагин панели.dashboard/plugin_api.py— серверные маршруты панели.
Ни одно из них не является обязательным; включайте только те уровни, которые вам нужны.
Справочник манифеста
{
"name": "my-plugin",
"label": "Мой плагин",
"description": "Что делает этот плагин",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills",
"override": "/",
"hidden": false
},
"slots": ["sidebar", "header-left"],
"entry": "dist/index.js",
"css": "dist/style.css",
"api": "plugin_api.py"
}
| Поле | Обязательно | Описание |
|---|---|---|
name | Да | Уникальный идентификатор плагина. Строчные буквы, дефисы допустимы. Используется в URL и регистрации. |
label | Да | Отображаемое имя, показываемое на вкладке навигации. |
description | Нет | Краткое описание (показывается в административных поверхностях панели). |
icon | Нет | Имя иконки Lucide. По умолчанию Puzzle. Неизвестные имена возвращаются к Puzzle. |
version | Нет | Строка Semver. По умолчанию 0.0.0. |
tab.path | Да | URL-путь для вкладки (например, /my-plugin). |
tab.position | Нет | Куда вставить вкладку. "end" (по умолчанию), "after:<path>" или "before:<path>" — значение после двоеточия — это сегмент пути целевой вкладки (без ведущего слеша). Примеры: "after:skills", "before:config". |
tab.override | Нет | Установите на путь встроенного маршрута ("/", "/sessions", "/config", ...), чтобы заменить эту страницу вместо добавления новой вкладки. См. Замена встроенных страниц. |
tab.hidden | Нет | Если true, регистрирует компонент и любые слоты без добавления вкладки в навигацию. Используется плагинами только со слотами. См. Плагины только со слотами. |
slots | Нет | Именованные слоты оболочки, которые заполняет этот плагин. Только для документации — фактическая регистрация происходит из JS-бандла через registerSlot(). Перечисление слотов здесь делает поверхности обнаружения более информативными. |
entry | Да | Путь к JS-бандлу относительно dashboard/. По умолчанию dist/index.js. |
css | Нет | Путь к CSS-файлу для внедрения в виде тега <link>. |
api | Нет | Путь к Python-файлу с маршрутами FastAPI. Монтируется по пути /api/plugins/<name>/. |
Доступные иконки
Плагины используют имена иконок Lucide. Панель сопоставляет их по имени — неизвестные имена молча возвращаются к Puzzle.
Текущее сопоставление: Activity, BarChart3, Clock, Code, Database, Eye, FileText, Globe, Heart, KeyRound, MessageSquare, Package, Puzzle, Settings, Shield, Sparkles, Star, Terminal, Wrench, Zap.
Нужна другая иконка? Откройте PR в ICON_MAP в web/src/App.tsx — чисто аддитивное изменение.
SDK плагина
Всё, что нужно плагину, находится на window.__VIBEOS_PLUGIN_SDK__. Плагины никогда не должны импортировать React напрямую.
const SDK = window.__VIBEOS_PLUGIN_SDK__;
// React + хуки
SDK.React // экземпляр React
SDK.hooks.useState
SDK.hooks.useEffect
SDK.hooks.useCallback
SDK.hooks.useMemo
SDK.hooks.useRef
SDK.hooks.useContext
SDK.hooks.createContext
// UI-компоненты (примитивы shadcn/ui)
SDK.components.Card
SDK.components.CardHeader
SDK.components.CardTitle
SDK.components.CardContent
SDK.components.Badge
SDK.components.Button
SDK.components.Input
SDK.components.Label
SDK.components.Select
SDK.components.SelectOption
SDK.components.Separator
SDK.components.Tabs
SDK.components.TabsList
SDK.components.TabsTrigger
SDK.components.PluginSlot // отобразить именованный слот (полезно для вложенных UI плагинов)
// VibeOS API-клиент + сырой загрузчик
SDK.api // типизированный клиент — getStatus, getSessions, getConfig, ...
SDK.fetchJSON // сырой fetch для пользовательских конечных точек (маршруты