Перейти к основному содержимому

Расширение панели управления

Веб-панель VibeOS (vibeos dashboard) спроектирована так, чтобы её можно было перекрашивать и расширять без форка кодовой базы. Доступны три уровня:

  1. Темы — YAML-файлы, которые перекрашивают палитру, типографику, макет и оформление отдельных компонентов панели. Положите файл в ~/.vibeos/dashboard-themes/ — он появится в переключателе тем.
  2. UI-плагины — каталог с manifest.json и JavaScript-бандлом, который регистрирует вкладку, заменяет встроенную страницу, дополняет её через слоты на уровне страницы или внедряет компоненты в именованные слоты оболочки.
  3. Серверные плагины — Python-файл внутри того же каталога плагина, который предоставляет router FastAPI; маршруты монтируются по пути /api/plugins/<name>/ и вызываются из UI плагина.

Все три уровня подключаются на лету: не нужно клонировать репозиторий, запускать npm run build или патчить исходники панели. Эта страница — канонический справочник по всем трём уровням.

Если вы просто хотите использовать панель, смотрите Веб-панель. Если хотите перекрасить терминальный CLI (не веб-панель), смотрите Скины и темы — система скинов CLI не связана с темами панели.

Как сочетаются части

Темы и плагины независимы, но синергичны. Тема может существовать сама по себе (просто YAML-файл). Плагин может существовать сам по себе (просто вкладка). Вместе они позволяют создать полную визуальную переработку с пользовательскими HUD — демо strike-freedom-cockpit (находится в сопутствующем репозитории vibeos-example-plugins — шаги по установке см. в разделе Комбинированное демо темы + плагина) делает именно это.


Содержание​


Темы​

Темы — это 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"> в &lt;head&gt; при переключении темы. Один и тот же 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 шрифта — таблица стилей шрифта внедряется как &lt;link&gt;, поэтому каталог сохраняет фиксированные источники внедрения. Для полностью пользовательского шрифта укажите fontSans+fontUrlв YAML темы, как показано выше.fontMono` темы (блоки кода, терминал) всегда остаётся нетронутым переопределением из интерфейса.

Макет​

КлючЗначенияОписание
radiusлюбая CSS-длина ("0", "0.25rem", "0.5rem", "1rem", ...)Токен радиуса углов. Отображается на --radius и каскадно распространяется на --radius-sm/md/lg/xl — все скруглённые элементы изменяются синхронно.
densitycompact | 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-&lt;name&gt;), которую могут читать встроенная оболочка и любой плагин. Слот bg` автоматически подключается к фону; остальные слоты предназначены для плагинов.

assets:
bg: "https://example.com/hero-bg.jpg" # автоматически подключается к &lt;Backdrop /&gt;
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-&lt;name&gt;-raw (нераспакованный URL) на случай, если плагину нужно передать его в <img src> вместо background-image.

Плагины читают их с помощью обычного CSS или JS:

// В слоте плагина
const hero = getComputedStyle(document.documentElement)
.getPropertyValue("--theme-asset-hero").trim();

Переопределение оформления компонентов​

componentStyles переопределяет стили отдельных компонентов оболочки без написания CSS-селекторов. Записи каждого блока становятся CSS-переменными (--component-&lt;bucket&gt;-&lt;kebab-property&gt;), которые считывают общие компоненты оболочки. Таким образом, переопределения card:применяются к каждому &lt;Card&gt;,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-&lt;kebab&gt; (например, 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 внедряется как один изолированный тег &lt;style data-vibeos-theme-css&gt; при применении темы и удаляется при переключении темы. Ограничение 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), загружают таблицу стилей по требованию — при первом переключении на них в &lt;head&gt; внедряется тег <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/&lt;name&gt;/ — расширение панели находится в подпапке 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.

Пропустите React.createElement

Если вы предпочитаете JSX, используйте любой сборщик (esbuild, Vite, rollup) с React в качестве внешней зависимости и выводом IIFE. Единственное жёсткое требование — чтобы конечный файл был одним JS-файлом, загружаемым через &lt;script&gt;. 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:&lt;path&gt;" или "before:&lt;path&gt;" — значение после двоеточия — это сегмент пути целевой вкладки (без ведущего слеша). Примеры: "after:skills", "before:config".
tab.overrideНетУстановите на путь встроенного маршрута ("/", "/sessions", "/config", ...), чтобы заменить эту страницу вместо добавления новой вкладки. См. Замена встроенных страниц.
tab.hiddenНетЕсли true, регистрирует компонент и любые слоты без добавления вкладки в навигацию. Используется плагинами только со слотами. См. Плагины только со слотами.
slotsНетИменованные слоты оболочки, которые заполняет этот плагин. Только для документации — фактическая регистрация происходит из JS-бандла через registerSlot(). Перечисление слотов здесь делает поверхности обнаружения более информативными.
entryДаПуть к JS-бандлу относительно dashboard/. По умолчанию dist/index.js.
cssНетПуть к CSS-файлу для внедрения в виде тега &lt;link&gt;.
apiНетПуть к Python-файлу с маршрутами FastAPI. Монтируется по пути /api/plugins/&lt;name&gt;/.

Доступные иконки​

Плагины используют имена иконок 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 для пользовательских конечных точек (маршруты