Design canvas — визуальные макеты для DeepSeek Harness
Плагин даёт две половины одной системы:
- инструменты агента
design_* — агент проектирует интерфейс как код: создаёт
холсты, собирает экраны батчами операций, объявляет компоненты, ведёт токены,
проверяет и экспортирует в HTML/CSS;
- панель «Макеты» в веб-интерфейсе — Figma-подобный редактор тех же самых
файлов: дерево слоёв, перетаскивание, ресайз, инспектор, библиотека
компонентов, редактор токенов, экспорт.
Человек и агент работают над одним документом на диске, поэтому дизайн не
расходится между «тем, что нарисовал агент» и «тем, что поправил человек».
Тот же редактор открывается и самостоятельно — отдельным окном на своём
порту, без харнесса: node standalone.js (см. Автономный редактор).
Содержание
Требования
- Node.js ≥ 20 (ESM,
node:fs/promises, fetch). Зависимостей у плагина нет:
React для автономного окна лежит в vendor/, Playwright нужен только для
браузерных проверок и берётся через npx.
- DeepSeek Harness — только для панели «Макеты» и инструментов агента.
Автономному редактору харнесс не нужен.
Быстрый старт
git clone https://github.com/kukuruzovo/dsh-design-canvas.git
cd dsh-design-canvas
node standalone.js # редактор на http://127.0.0.1:5178/
В окне: «Новый холст» → выберите каталог проекта → холст появится в
<проект>/.design/. Дальше его можно править руками, агентом или экспортировать.
Чтобы подключить плагин к DSH (инструменты design_* + панель «Макеты»):
dsh plugin --profile web add link:/абсолютный/путь/к/dsh-design-canvas
и перезапустить dsh web. Подробности — в разделе
Установка и обновление.
Где лежат макеты
Холсты живут в репозитории проекта, а не в домашнем каталоге DSH:
<корень проекта>/.design/
project.json — метаданные проекта (появляется при первой записи холста)
home.canvas.json — документ холста
settings-mobile.canvas.json
exports/ — сгенерированные HTML/CSS
home.html
home.css
home.components.html
Следствия: дизайн версионируется вместе с кодом, едет в git, переносится между
машинами и лежит рядом с приложением, для которого сделан.
Реестр корней — ~/.dsh/design-canvas.json. Встроенного списка проектов нет:
каталог попадает в реестр, когда вы инициализируете его как проект
(design_projects init, первый design_create или standalone.js --project), а
каталог с уже закоммиченной папкой .design/ плагин находит и сам. Найденный
проект потом адресуется по id — сценарий «клонировали репозиторий с
закоммиченными макетами». Корзина удалённых холстов — ~/.dsh/design-trash/
(восстановимо).
Модель документа
Документ — обычный JSON. Координатная модель Figma-подобная: у каждого узла
абсолютные x/y относительно родителя и свои w/h; у фрейма есть режим
layout.mode (none / vertical / horizontal), который включает поток и
пересчёт позиций детей. Файл холста <id>.canvas.json выглядит так:
{
"format": "dsh-design-canvas",
"version": 1,
"id": "home",
"name": "Home",
"device": "mobile",
"rev": 7, // ревизия: поле файла, а не модели документа
"tokens": { "color": { "bg": "#0b0d10" }, "text": { "h1": { "size": 32, "weight": 700 } } },
"components": {
"button-primary": {
"key": "button-primary",
"name": "Button / Primary",
"node": { /* шаблон узла без координат */ },
"props": {
"label": { "type": "text", "default": "Кнопка", "target": "text", "property": "text.value" }
},
"variants": { "ghost": { "name": "Ghost", "node": { /* свой шаблон */ }, "props": { "label": { "default": "Позже" } } } }
}
},
"guides": { "x": [120, 340], "y": [64] }, // направляющие редактора; нет разметки — нет поля
"root": { "id": "root", "type": "frame", "children": [ /* фреймы-экраны */ ] }
}
Типы узлов: frame, group, rect, ellipse, line, text, image,
instance.
rev живёт только в файле: design_read mode=full и GET /canvas возвращают
документ без него, а сама ревизия приходит соседним полем ответа.
Пределы: вложенность — 64 уровня (глубже дети обрезаются), узлов — 20 000,
вложенные инстансы разворачиваются до 32 уровней, холст больше 8 МБ не читается
и не записывается (запись отклоняется, чтобы файл не стал нечитаемым).
Токены
Значение поля может быть литералом (#0b0d10) или ссылкой на токен
(token:color.bg). Ссылка разрешается при отрисовке, экспорте и проверке, а в
CSS превращается в переменную: var(--color-bg, #0b0d10). Смена токена
перекрашивает весь макет — и в редакторе, и в собранном HTML.
Единицы вывода берутся из пути токена: radius.*, space.*, размеры и
межбуквенные интервалы типографики, а также смещения теней получают px;
веса и множители строки остаются безразмерными.
Исключение — поле text.token: это голый путь токена типографики
(text.h1, без префикса token:), потому что токен составной и описывает
сразу размер, вес, высоту строки и трекинг. Явные text.size, text.weight,
text.lineHeight, text.letterSpacing переопределяют значения токена для
конкретного узла.
Раскладка
layout.mode = 'none' — свободное позиционирование: дети сохраняют авторские
x/y. vertical / horizontal включает поток: позиции детей выводятся из
порядка, gap, padding, align и justify, а размер фрейма подгоняется по
содержимому (отключить можно полями узла autoW / autoH со значением false —
они сохраняются в canvas.json и переживают запись).
justify (start / center / end / space-between) распределяет свободное
место по главной оси, align: 'stretch' растягивает детей по поперечной. Свободное
место появляется только у фрейма с авторским размером по этой оси: с включённым
autoW/autoH фрейм сжимается по содержимому и распределять нечего. Поэтому
justify: center работает вместе с autoW: false (для горизонтали) и autoH: false
(для вертикали), а align: 'stretch' — с заданным размером по поперечной оси.
Ту же арифметику повторяет панель, поэтому сохранённый файл совпадает с картинкой.
Направляющие
guides — разметка редактора: x задаёт вертикальные линии, y — горизонтальные, в
координатах холста. Поле необязательное: холст без разметки его не содержит, и нормализация
не добавляет пустые массивы в старые canvas.json. Значения сортируются, дубликаты
отбрасываются, на ось хранится не больше сотни линий. В экспорт направляющие не попадают:
это разметка рабочего места, а не часть макета. Агент ставит их операцией set-guides.
Компоненты
Инстанс — живая ссылка, а не копия. Своего содержимого у инстанса нет: ключ компонента,
координаты и overrides; содержимое разворачивается из шаблона при отрисовке и экспорте, поэтому
правка компонента обновляет все инстансы сразу. Авторские дети инстанса не отрисовываются и не
экспортируются, а design_validate сообщает о них замечанием instance-children.
design_components action=detach (или op: detach) превращает инстанс в
обычный узел.
Правка компонента. update-component меняет имя, описание, параметры, вариации и шаблон
существующего компонента, не пересоздавая его: props сливаются по имени параметра, а сам параметр —
по полям ({ label: { default: 'ОК' } } меняет только значение по умолчанию и не теряет target),
значение null убирает параметр, node заменяет шаблон целиком.
Вариации. variants[name] — тот же компонент с другим шаблоном (node) и/или другими полями
параметров (props). Параметры и переопределения инстанса общие для всех вариаций, поэтому
переключение вариации не теряет правки инстанса: инстанс хранит поле variant, а разворачивание
берёт шаблон вариации (componentTemplate) и действующие параметры (componentProps). Ссылка на
несуществующую вариацию попадает в design_validate замечанием missing-variant, а лист
библиотеки в экспорте печатает базовый шаблон и каждую вариацию отдельной ячейкой. define-component умеет объявить
компонент не только из узла холста (from), но и из готового шаблона (node) — так панель
клонирует компонент, не оставляя мусорного узла в макете.
Переопределения инстанса бывают двух видов:
- объявленный параметр компонента — значение ложится в
property-путь узла-цели
target (root, #Имя узла, text или путь внутри шаблона —
children[0].label);
- произвольный путь узла (
style.fill, text.value, w) — точечная правка без
объявления параметра.
При объявлении компонента параметр label выводится автоматически из первого
текстового узла.
Инструменты агента
| Инструмент | Назначение |
|---|
design_projects | реестр проектов и холстов; init — инициализировать каталог как проект |
design_create | создать холст (устройство, шаблон, сразу батч операций) |
design_read | проекции холста: summary, outline, node, tokens, components, full |
design_apply | основной инструмент: батч операций, транзакционно |
design_tokens | чтение и правка палитры, типографики, радиусов, отступов, теней |
design_components | list / get / define / update / instantiate / detach / delete |
design_export | HTML + CSS + страница библиотеки компонентов в .design/exports/ |
design_validate | дубли id, битые инстансы, авторские дети инстанса, отсутствующие токены, обрезка, контраст (порог по размеру текста, токены и rgba() считаются) |
design_delete | корзина: list / delete / restore |
Операции design_apply
| Операция | Поля |
|---|
add | { parent, node | type, index?, after? } — занятый id заменяется новым, ответ возвращает фактический |
update | { id | ids, patch } — x, y, w, h, name, visible, locked, style.*, text.*, layout.* |
move | { id, parent?, index?, after? } — after: id соседа, за которым вставить; без parent узел остаётся у того же родителя и меняет порядок |
delete | { id | ids } |
duplicate | { id | ids, count?, offsetX?, offsetY?, parent? } |
group / ungroup | { ids, name?, as? } / { id } — as: frame оборачивает выделение во фрейм по габаритам (по умолчанию группа); неизвестное as — ошибка |
reorder | { id, index | direction }, direction: front / back / up / down |
set-tokens | { tokens, remove?, replace? } — remove удаляет пути (color.bg, text.h1.size) |
define-component | { key, from?, node?, name?, description?, props?, convert? } — from берёт узел холста, node задаёт готовый шаблон |
update-component | { key, name?, description?, props?, variants?, node? } — props сливаются по имени (значение null убирает параметр), variants — по имени вариации (значение null убирает вариацию), node заменяет шаблон |
instantiate | { component, parent, x?, y?, w?, h?, name?, variant?, overrides?, index?, after? } — variant выбирает вариацию |
Батч транзакционный: ошибка в любой операции откатывает весь батч, документ
остаётся прежним. Ревизия растёт на 1; expectRev защищает от затирания правок,
сделанных в панели (несовпадение → отказ 409).
Фрейм, добавленный операцией add, по умолчанию обрезает детей (clip: true),
как фрейм из шаблона устройства; clip: false делает контейнер прозрачным.
Пример: экран одним вызовом
// design_create { "project": "my-app", "canvas": "home", "device": "mobile" }
// design_apply
{
"project": "my-app",
"canvas": "home",
"ops": [
{ "op": "add", "parent": "page", "node": {
"id": "header", "type": "frame", "name": "Header",
"x": 0, "y": 0, "w": 390, "h": 88,
"style": { "fill": "token:color.surface" } } },
{ "op": "add", "parent": "header", "node": {
"id": "title", "type": "text", "name": "Title",
"x": 20, "y": 32, "w": 300, "h": 32,
"text": { "value": "Викторины", "token": "text.h1", "color": "token:color.fg" } } },
{ "op": "add", "parent": "page", "node": {
"id": "cta", "type": "frame", "name": "CTA",
"x": 20, "y": 120, "w": 350, "h": 52,
"style": { "fill": "token:color.accent", "radius": "token:radius.pill" },
"layout": { "mode": "horizontal", "align": "center", "justify": "center" } } },
{ "op": "add", "parent": "cta", "node": {
"type": "text", "name": "CTALabel", "x": 0, "y": 0, "w": 200, "h": 24,
"text": { "value": "Начать", "color": "token:color.accentFg", "align": "center" } } },
{ "op": "define-component", "key": "button-primary", "from": "cta", "name": "Button / Primary" },
{ "op": "instantiate", "component": "button-primary", "parent": "page",
"x": 20, "y": 190, "overrides": { "label": "Мои результаты" } }
]
}
Экспорт
design_export пишет в <проект>/.design/exports/:
<canvas>.html — страница со всеми фреймами;
<canvas>.css — токены как :root { --color-bg: … } плюс базовые классы;
<canvas>.components.html — библиотека: каждый компонент один раз, а его вариации — отдельными ячейками.
Верхние фреймы холста — экраны: на собранной странице они всегда встают потоком
доски (.dc-board), поэтому два экрана, поставленные в одну точку (0,0), не
накладываются друг на друга. Режим решает, как позиционируются узлы внутри:
fidelity — абсолютное позиционирование узлов внутри фреймов, пиксельно
точно (превью, handoff); верхние фреймы и здесь встают потоком доски, поэтому их
координаты в собранной странице не сохраняются;
clean — фреймы с layout.mode становятся flex-контейнерами, остальное
абсолютное (ближе к вёрстке приложения).
У каждого элемента есть data-node="<id узла>" — DOM можно связать с макетом.
Панель «Макеты»
Иконка в левой колонке веб-интерфейса. Внутри: тулбар инструментов (select,
frame, rect, ellipse, text), панели «Холсты / Слои / Компоненты / Токены»,
вьюпорт с pan/zoom, инспектор, автосохранение с разрешением конфликтов ревизий,
undo/redo, экспорт и проверка. Узлы line и image из тулбара не создаются —
их ставит агент операцией add.
Правка узлов
- Рамка выделения — протяжка левой кнопкой по пустому месту или по фону
контейнера: протяжка по фону фрейма выделяет его детей, а клик по тому же фону —
сам фрейм. Верхние фреймы выделяются рамкой из серого поля вокруг макета, дети —
рамкой внутри фрейма. Панорама холста: средняя кнопка мыши или пробел с протяжкой;
зум — Ctrl+колесо или кнопки тулбара.
- Мультивыделение —
Shift+клик или Ctrl/Cmd+клик на холсте и в дереве слоёв
добавляет узел к выделению и снимает его повторным кликом; Ctrl+A выделяет всё
в текущей области (дети «вошедшего» контейнера, иначе верхние фреймы). Выделенные
узлы двигаются, ресайзятся, копируются и группируются вместе; если в выделении
есть предок и его потомок, потомок едет вместе с предком.
- Вход в контейнер — двойной клик по фрейму или группе: протяжка по его фону
делает рамку по его детям, а не двигает контейнер. Выход —
Esc.
- Стрелки двигают выделение на 1px, с
Shift — на 10px. При перетаскивании
Shift ведёт узел строго по одной оси — по той, куда курсор ушёл сильнее, поэтому
объект скользит вдоль направляющей и не сносит в сторону. Внутри фрейма с
авто-раскладкой координаты детей считает сама раскладка (layout.mode), поэтому
перетаскивание ребёнка там ничего не меняет: правьте порядок слоёв, gap и padding.
- Копирование —
Ctrl+C / Ctrl+V, дублирование — Ctrl+D, вырезать — Ctrl+X.
Копия встаёт рядом с оригиналом (в его родителя), каждая следующая вставка
сдвигается на шаг.
- Группировка —
Ctrl+G и Ctrl+Shift+G; объединить выделение в новый фрейм —
Ctrl+Alt+G или пункт контекстного меню «Объединить в фрейм» (фрейм встаёт по
габаритам выделения, без авто-раскладки). Порядок слоёв — Ctrl+] / Ctrl+[
(с Shift — сразу на передний или задний план).
- Выравнивание — секция в инспекторе при выделении двух и более узлов: шесть
режимов и распределение по горизонтали и вертикали (нужно минимум три узла).
- Контекстное меню по правой кнопке на узле (на холсте или в дереве слоёв):
дублирование, копирование, вставка, группировка, «объединить в фрейм», порядок,
замок, видимость, отсоединение инстанса, «сделать компонентом», удаление. Правый
клик по узлу вне выделения сначала выделяет его.
Компоненты в панели
- Библиотека — карточки с настоящими превью: миниатюра рисуется тем же
NodeView, что и холст,
поэтому не расходится с макетом. Есть поиск по имени и ключу, счётчики инстансов и параметров,
наведение на карточку подсвечивает все инстансы компонента на холсте.
- Карточка компонента — превью крупнее, имя и описание, редактор параметров (имя, тип,
значение по умолчанию, цель и свойство; цель подставляется кликом по имени узла шаблона),
вариации (превью каждой, добавление вариации из выделенного узла, удаление), кнопки
«поставить инстанс», «найти инстансы» и «дублировать компонент».
- Инстанс — в инспекторе видно, какие параметры переопределены на этом инстансе, и есть
сброс одного переопределения, сброс всех, «вынести правки в компонент» (шаблон становится тем,
что нарисовано на инстансе), замена компонента у инстанса с переносом совпадающих
переопределений и переключатель вариации, когда они у компонента объявлены.
Разметка рабочего места
- Наведение подсвечивает узел под курсором, его размер и всю цепочку контейнеров
до верхнего фрейма — структура читается без клика по дереву слоёв. Та же цепочка
подсвечивается и в дереве слоёв, а наведение в дереве подсвечивает узел на холсте.
- Линейки сверху и слева показывают координаты холста; шаг штрихов зависит от
масштаба, подписи — каждые десять штрихов. Кнопка «Линейки» или
Shift+R скрывает их.
- Направляющие: потяните из линейки, чтобы поставить линию. Линия переносится
перетаскиванием, удаляется броском обратно на линейку, двойным кликом или кнопкой
«Убрать все направляющие». Направляющие видны по кнопке «Линии» и хранятся в
canvas.json.
- Привязка работает при перемещении, ресайзе и создании узла: объект липнет к краям и
серединам родителя, соседей и соседних фреймов, к направляющим и к пиксельной сетке
(при ресайзе и создании
Shift — шаг 10px). Совпавшие линии подсвечиваются. Привязка
к объектам важнее привязки к сетке, а Alt отключает обе; кнопка «Привязка» выключает
привязку целиком. При перемещении притягиваются только те оси, по которым объект реально
сдвинулся, — на старте жеста узел не уводит в сторону.
- Замеры показывают расстояния: у выделенного узла — до краёв родителя и до ближайших
соседей, при перетаскивании — живые значения, при наведении — по удержанию
Alt.
Padding контейнера выделяется подложками вокруг его содержимого, поэтому видно и отступы
контейнера, и зазоры между объектами.
Автономный редактор
Тот же редактор открывается отдельным окном на своём порту и работает без
харнесса — нужен только Node:
node standalone.js # http://127.0.0.1:5178/
npm run editor # то же самое
node standalone.js --port 4180 --open # другой порт и сразу открыть браузер
node standalone.js --project ~/app/foo # зарегистрировать корень проекта в реестре
node standalone.js --tailscale # окно для телефона и других устройств tailnet
node standalone.js --host 0.0.0.0 --token # наружу — только с токеном
Редактор с телефона: --tailscale
node standalone.js --tailscale # или: npm run editor:tailscale
Сервер слушает адрес этого узла в tailnet (100.x.y.z, узнаётся у
tailscale ip -4, а без CLI — по интерфейсу tailscale0), поэтому окно
открывается с телефона, ноутбука или другого узла сети, а в локальную сеть
192.168.* не выставляется вовсе. В выводе печатаются два адреса: по IP и по
имени MagicDNS (<узел>.<tailnet>.ts.net) — второй удобнее, но требует
включённого MagicDNS.
Доступ при этом закрыт токеном: он сохраняется в
$DSH_HOME/design-canvas-token (права 0600), поэтому адрес окна не меняется
от запуска к запуску — его можно положить в закладку на телефоне. Свой токен
задаётся --token <значение>, сменить сохранённый — удалить файл.
HTTPS через tailnet — флагом --tailscale-serve: сервер сам позовёт
tailscale serve --bg <адрес> и напечатает https://<узел>.<tailnet>.ts.net/.
Эта команда требует root, поэтому без него сервер не падает, а печатает готовую
строку для sudo и напоминание sudo tailscale serve reset, когда прокси
больше не нужен.
Это не вторая реализация: standalone.js отдаёт ту же страницу редактора
(client.js) и монтирует тот же обработчик /api/dsh-design-canvas, что и
плагин внутри DSH. Обе половины ходят через один service.js и правят одни
файлы <проект>/.design/*.canvas.json, поэтому холст, открытый в отдельном
окне, виден агенту и панели «Макеты» — и наоборот.
Что внутри автономного окна:
standalone/index.html + standalone/shell.js — оболочка: она ставит шим
window.__ModuleLoader__ и контекст с сервисами slots/locale, которых
клиентская половина плагина ждёт от харнесса, и переключатель светлой/тёмной темы;
standalone/theme.css — переменные --dsw-* для светлой и тёмной темы
(значения сняты с темы харнесса, чтобы панель выглядела одинаково там и здесь);
vendor/react.production.min.js, vendor/react-dom.production.min.js —
React 18 локально (MIT), поэтому окно не зависит ни от сети, ни от установки DSH.
Порт по умолчанию — 5178, адрес — только этот компьютер: маршрут умеет писать
файлы в .design/ проектов. Как только окно становится видно не только отсюда
(--tailscale, --tailscale-serve или не-петлевой --host), доступ закрывается
токеном: свой из --token (8–128 символов из латиницы, цифр и . _ ~ -) или
сохранённый автоматически. Страница с ?token= выдаёт cookie, а скрипты могут
ходить с заголовком x-dsh-token. Реестр проектов и корзина — те же
(~/.dsh/design-canvas.json, ~/.dsh/design-trash/), так что окно и харнесс
видят один список проектов.
HTTP-маршрут
/api/dsh-design-canvas — тот же service.js, что и у инструментов агента.
Основное:
GET /projects?canvases=1 GET /canvases?project=
GET /display?project=&id= GET /canvas?project=&id=&mode=full
GET /outline|tokens|components|validate|export|trash|config
POST /create | /init | /apply | /canvas | /tokens | /components | /export | /delete | /restore
GET /display отдаёт готовое дерево для отрисовки: все инстансы развёрнуты
сервером, дети инстанса неймспейснуты его id (<instanceId>/<childId>) — так
правила разворачивания компонентов живут в одном месте и не дублируются на
клиенте.
Дефолты адаптеров различаются: design_read читает summary, GET /canvas —
full; GET /export всегда отдаёт превью со встроенным CSS и файлов не пишет.
Архитектура
| Файл | Слой |
|---|
schema.js | модель: нормализация, обход дерева, токены, компоненты, направляющие, валидация |
store.js | файлы: реестр проектов, .design/, ревизии, корзина |
ops.js | операции над документом (транзакционный движок) |
export.js | генерация HTML/CSS |
service.js | сценарии: одна логика для агента и панели |
tools.js | 9 инструментов агента (тонкие адаптеры) |
route.js | HTTP-маршрут панели (тонкий адаптер) |
index.js | точка входа плагина: инструменты + маршрут |
client.js | визуальный редактор (React без JSX) |
standalone.js | автономный хост: статика оболочки, тот же маршрут на своём порту, токен, tailnet |
standalone/ | оболочка окна (index.html, shell.js) и тема --dsw-* |
vendor/ | React 18 UMD локально (MIT) — окно не зависит от харнесса и сети |
Правило: логика живёт в schema/store/ops/export/service, а tools.js,
route.js и standalone.js только разбирают аргументы и рендерят результат.
Демо
node demo.mjs собирает холст demo/.design/quiz-home.canvas.json — экран
приложения с токенами, компонентом topic-card (три инстанса с разными
переопределениями) и потоковой нижней навигацией. Он же служит фикстурой для
проверки панели.
Превью снимаются с того же экспорта: npm run demo && npm run preview.
npm run demo пересобирает только demo/.design/, поэтому готовые превью
уцелевают; npm run preview нужен, когда изменился сам демо-холст или экспорт.
Снимок самой панели «Макеты» делает живой прогон — DSH_WEB_TOKEN=… node smoke-gui.mjs … .gui-shot.png.
Проверки
npm run check # синтаксис всех .js/.mjs
npm test # ядро + инструменты и маршрут + контракт клиента + автономный сервер
npm run test:editor # автономное окно редактора в Playwright
npm run test:editor -- --tailscale # то же окно по адресу tailnet (если Tailscale есть)
DSH_WEB_TOKEN=<токен из адресной строки> \
node smoke-gui.mjs http://127.0.0.1:3080 .gui-shot.png # панель в живом DSH
smoke.mjs — схема, хранилище, операции, транзакционность, экспорт, защита путей;
smoke-tools.mjs — регистрация 9 инструментов и полный цикл через HTTP-маршрут;
smoke-client.mjs — контракт браузерного модуля: слоты, словари, маршруты API, отсутствие JSX;
smoke-standalone.mjs — автономный сервер: статика оболочки, вендорный React, тема,
правило доступа (токен, как только окно видно не только с этого компьютера), сохранение
токена, защита от обхода пути, полный цикл правки через /api/dsh-design-canvas и аргументы
CLI; если на машине есть Tailscale, отдельно проверяется --tailscale, иначе шаг помечается
пропущенным;
smoke-editor.mjs — поднимает автономное окно на свободном порту с временным DSH_HOME,
повторяет сценарий редактора в браузере и снимает .editor-shot.png; харнесс не нужен,
Playwright берётся из установки или кеша npx. С флагом --tailscale то же окно
открывается по адресу tailnet — так проверяется сценарий «с другого устройства»;
smoke-gui.mjs — открывает панель в живом DSH и снимает скриншот, собирая ошибки консоли.
Ему нужны запущенный DSH, токен и собранная демо-фикстура (npm run demo).
Разработка
AGENTS.md — правила проекта для агента: слои, инварианты, команды, критерии сдачи;
docs/ARCHITECTURE.md — внутреннее устройство: слои, модель, рецепты расширения, коды ошибок;
docs/BACKLOG.md — известные дефекты и долги с уровнем доказанности;
docs/PLAN-editor-ux.md — план улучшения редактора и статусы фаз;
.dsh/skills/ — скилы проекта, которые DSH подхватывает автоматически;
agency/ — эксперты и команда The Agency для разработки плагина
(npm run agency:check, затем node tools/agency-sync.mjs).
Установка и обновление
Плагин — обычный npm-пакет: Host-половина (index.js + dsh.bundle.patch) и
клиентская (client.js + dsh.client). Зависимостей нет, поэтому установка
сводится к тому, чтобы добавить каталог в профиль DSH.
git clone https://github.com/kukuruzovo/dsh-design-canvas.git
dsh plugin --profile web add link:/абсолютный/путь/к/dsh-design-canvas
dsh web # перезапуск: Host-половина подхватывается при старте
dsh plugin — это обёртка над pnpm в каталоге профиля, поэтому link: ставит
пакет без копирования и без публикации в реестр npm. В веб-интерфейсе то же
делает plugin_manager (install_bundle с абсолютным путём к каталогу).
После установки:
- правки Host-половины подхватываются перезапуском
dsh web;
- правки
client.js — перезагрузкой страницы (на лету — только при запущенном
pnpm run dev:web в чекауте харнесса);
- обновление —
git pull в каталоге плагина плюс перезапуск dsh web.
Автономному окну (node standalone.js) установка в DSH не нужна вовсе: это
обычный Node-процесс в каталоге плагина, зависимостей у него нет — React лежит
рядом в vendor/. Правки client.js и оболочки видны в нём после перезагрузки
страницы, правки standalone.js — после перезапуска процесса.
Лицензия
MIT. Вендорный React 18 в vendor/ распространяется по MIT
(тексты лицензий лежат рядом).