20. Виджеты
Виджет определяет, как модуль или интерфейс отрисовывается на диаграмме. До этого момента всё отрисовывалось со значениями по умолчанию из стандартной библиотеки — прямоугольники, помеченные ключевым словом вида. Эта глава о том, как заменить эти значения по умолчанию на пользовательскую визуализацию.
Поддерживаются две формы, обе через единое поле widget:. Парсер различает их по виду значения:
| Форма | Синтаксис | Лучше всего подходит для |
|---|---|---|
| Пользовательский элемент | widget: <identifier> (+ widget.<prop>: …) | Богатых виджетов с состоянием, анимацией, структурированными данными |
| Встроенный шаблон | widget: "<inline html>" | Статических карточек-идентификаторов, бейджей, простого SVG, без JS-сопроводителя |
Обе формы каскадируют через цепочку типов и переопределяются ровно так же, как любое другое поле. cascade widget: ... на типе вида штампует каждый экземпляр; экземпляр может переопределить тег, переключиться на шаблон или подкрутить отдельные свойства.
Пользовательские элементы
service Orders { widget: arch-service widget.icon: server widget.accent: "#34d399"
team: Platform
labels { domain: ordering }}Рендерер выдаёт <arch-service ...>, когда этот узел монтируется. Каждое поле widget.<name> становится HTML-атрибутом в kebab-case (widget.icon → icon="server"). JS пользовательского элемента регистрируется в скрипте виджетов вашего пакета (объявленном через widgets: в package.archspace — см. главу 12).
Минимальная регистрация:
class ArchMicroservice extends HTMLElement { static get observedAttributes() { return ["name", "icon", "accent"]; } attributeChangedCallback() { this.render(); } connectedCallback() { this.render(); }
render() { const root = this.shadowRoot ?? this.attachShadow({ mode: "open" }); root.innerHTML = ` <style>:host { display: block; }</style> <div class="card">…</div> `; }}customElements.define("arch-service", ArchMicroservice);Что получает виджет
Рендерер предоставляет модель и состояние области просмотра через атрибуты, CSS-переменные и DOM-свойства:
| Поверхность | Содержимое |
|---|---|
Атрибут name | Имя экземпляра |
Атрибут data-zoom-level | "dot" / "compact" / "full" — текущая полоса области просмотра |
Атрибут data-selected | "true", когда это выбранный модуль |
Атрибут data-highlighted | "true", когда узел находится в активном радиусе воздействия |
CSS-переменная --arch-projected-width | Спроецированная ширина в пикселях (непрерывная) |
CSS-переменная --arch-scale | Масштаб области просмотра (1 = пространство модели) |
CSS-переменная --arch-band-compact | 0→1 по мере входа узла в полосу «compact-readable» |
CSS-переменная --arch-band-full | 0→1 по мере входа узла в полосу полной детализации |
Атрибуты widget.<...>-свойств | Каждое свойство в kebab-case, значение приведено к строке |
Свойство el.arch | Полный разрешённый модуль: { id, name, kindName, fields, labels, facets, interfaces, … } |
Свойство el.archZoom | { level, projectedWidth, scale } |
Свойство el.archState | { selected, highlighted } |
DOM-свойства устанавливаются после connectedCallback. Перерисовывайте в сеттере свойства или используйте attributeChangedCallback для простых случаев.
Размер узла
Каждый виджет отрисовывается в прямоугольник. По умолчанию это 220 × 140 пикселей. Виджеты могут объявить своё значение по умолчанию через статические свойства класса:
class ArchSystem extends HTMLElement { static archWidth = 420; static archHeight = 300; // ...}customElements.define("arch-system", ArchSystem);Экземпляры могут переопределить:
type system payments { widget.width: 600 widget.height: 400}Порядок разрешения по каждой оси: поле экземпляра → статика виджета → запасное значение (220 × 140).
Отрисовка с учётом масштаба
Рендерер предоставляет масштаб тремя способами. Выбирайте тот, что подходит под эффект:
- Дискретные полосы — атрибут
data-zoom-levelна хосте (dot/compact/full). Используйте для пошаговых переключений, где сам скачок и есть визуальный эффект. - Непрерывные полосы — CSS-переменные
--arch-band-compactи--arch-band-full, каждая плавно интерполируется0..1в своём диапазоне. Используйте для flex-grow, размера шрифта или прозрачности, которые должны переходить без скачка. - Сырая проекция —
--arch-projected-width(пиксели) и--arch-scale(множитель). Используйте для выраженийclamp()/calc().
Рекомендуемый паттерн использует структурные атрибуты (header, body, details), привязанные к переменным полос:
<div class="card"> <div header><span class="icon">▣</span><span class="name">{{name}}</span></div> <div body><slot></slot></div> <div details>team · {{team}}</div></div>[header] { flex: 1 0 0; min-height: 0; }[body] { flex: calc(2 * var(--arch-band-compact, 0)) 0 0; min-height: 0; }[details] { flex: var(--arch-band-full, 0) 0 0; min-height: 0; }Заголовок заполняет карточку при масштабе «точка»; тело подрастает по мере того, как узел становится compact-читаемым; детали появляются при полном масштабе. Промежуточные кадры интерполируются непрерывно — CSS-переходы не нужны.
Выделение и подсветка
Состояние проявляется как атрибуты хоста:
:host([data-selected="true"]) .card { box-shadow: 0 0 0 2px var(--accent), 0 0 24px var(--accent-glow);}:host([data-highlighted="true"]) .card { box-shadow: 0 0 0 2px var(--warn);}Точки крепления рёбер
Помечайте точки подключения рёбер через data-anchor="in" / data-anchor="out". Движок раскладки запрашивает getBoundingClientRect() у этих узлов:
<div class="anchor" data-anchor="in" style="position:absolute; left:-4px; top:50%"></div><div class="anchor" data-anchor="out" style="position:absolute; right:-4px; top:50%"></div>Вложенные дети — паттерн со слотом
Когда у модуля есть дети, рендерер передаёт их как дочерние элементы light-DOM элемента виджета. Виджеты, которые хотят содержать своих детей, объявляют <slot>:
class ArchSystem extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }).innerHTML = ` <style> :host { display: block; width: 100%; height: 100%; } .frame { display: flex; flex-direction: column; padding: 0.6rem; } [header] { flex: 1 0 0; } [body] { flex: calc(4 * var(--arch-band-compact, 0)) 0 0; min-height: 0; } </style> <div class="frame"> <div header><strong>{{name}}</strong></div> <div body><slot></slot></div> </div> `; }}customElements.define("arch-system", ArchSystem);Виджеты, не объявившие слот, просто не показывают своих детей — то же поведение по умолчанию, что и у любого HTMLElement с непроецированным light-DOM.
Встроенные шаблоны
Когда вам просто нужна карточка с пользовательским видом без JS-сопроводителя, используйте строковое значение:
service Cart { team: shop
labels { domain: shopping }
widget: " <div class='w-full h-full p-3 rounded-xl bg-arch-card border border-purple-400/50'> <div class='font-semibold text-purple-200'>{{name}}</div> <div class='text-xs text-slate-400'>{{kindName}} · {{team}}</div> <div class='text-xs text-purple-400/80'>{{labels.domain}}</div> </div> "}Парсер видит, что значение — строковой литерал, а не идентификатор, и направляет его рендереру шаблонов.
Строки в тройных кавычках
Для шаблонов, содержащих двойные кавычки (HTML-атрибуты), используйте строки в тройных кавычках:
service Cart { widget: """ <archui-card variant="outline" accent="#a78bfa"> <archui-header slot="header" name="{{name}}"></archui-header> </archui-card> """}Строки в тройных кавычках — сырые: обратные слеши не являются escape-последовательностями, а интерполяция вида ${expr} намеренно не поддерживается.
Правила подстановки
Подстановка в шаблоне строгая и не содержит логики. Нет ни условных операторов, ни циклов, ни форматтеров — только поиск по {{path}}:
| Токен | Разрешается в |
|---|---|
{{name}} | Имя экземпляра |
{{kindName}} (или {{kind}}) | Ключевое слово вида |
{{description}} | Склеенные описания |
{{<dotted.field>}} | Поле по пути |
{{labels.<dotted>}} | Метка по пути |
{{{<path>}}} | То же, что выше, но сырое (пропускает экранирование HTML) |
{{widget.<...>}} | Всегда пусто — конфигурация виджета никогда не утекает в содержимое |
Неизвестные пути отрисовываются как пустая строка. Двойные фигурные скобки экранируют; тройные внедряют сырой HTML.
CSS для встроенных шаблонов
Утилиты Tailwind, используемые в шаблонах, компилируются на стороне сервера через UnoCSS (совместимый с Tailwind). Сервер обходит каждый строковый шаблон widget:, извлекает имена классов-кандидатов и выставляет скомпилированный CSS по GET /api/widgets/css. Просмотрщик подгружает его при старте и перезапрашивает при обновлениях рабочего пространства.
Пользовательские утилиты arch-* поставляются «из коробки»:
| Класс | Эффект |
|---|---|
bg-arch-card | Предварительно настроенный тёмный градиент для карточек |
bg-arch-card-hot | Вариант с оттенком индиго |
shadow-arch-glow / -lg | Лёгкое акцентное свечение |
shadow-arch-card | Тень и внутренняя подсветка |
text-arch-{50..950} | Расширенная палитра небесно-голубого |
Библиотека примитивов arch.ui
Стандартная библиотека поставляет библиотеку примитивов arch.ui. Виджеты видов и встроенные шаблоны компонуют её одинаково — по имени тега:
service Cart { team: shop
labels { domain: shopping tech: "rust, postgres" }
widget: """ <archui-card variant="outline" accent="#a78bfa"> <archui-header slot="header" icon="server" name="{{name}}"></archui-header> <archui-body slot="body"> <archui-tech-stack></archui-tech-stack> </archui-body> <archui-details slot="details"> <archui-field path="team" label="team"></archui-field> <archui-label path="domain" format="mono"></archui-label> </archui-details> <archui-anchor side="in"></archui-anchor> <archui-anchor side="out"></archui-anchor> </archui-card> """}Токены темы живут под пользовательскими CSS-свойствами --au-*. Переопределяйте их на любом хосте, чтобы перетематизировать поддерево:
:host { --au-accent: #a78bfa; }Токены наслоены через CSS @layer archui.tokens, archui.project;, так что токены проекта выигрывают у значений по умолчанию из стандартной библиотеки независимо от порядка импорта.
Каскад
И widget, и widget.<...> — обычные поля, поэтому к ним применимы механизмы распространения из главы 18:
type module service { cascade widget: arch-service // every service gets this cascade widget.accent: "#34d399" // ...with this default accent}
type service fancy_service { cascade widget: arch-fancy-service}
fancy_service Orders { team: platform widget.accent: "#a78bfa" // instance-level override}Стандартная библиотека (arch.modules) каскадирует widget: arch-<kind> по умолчанию на каждый объявляемый вид — service → arch-service, database → arch-database и т. д. Проект, делающий use * from arch.modules, получает работающую визуализацию бесплатно, не написав ни одного скрипта виджета.
Горячая перезагрузка
Сервер наблюдает за .arch, package.archspace и widgets.js каждого загруженного пакета. В просмотрщик через /api/events (SSE) поступают три канала событий:
| Событие | Когда | Реакция просмотрщика |
|---|---|---|
connected | Рукопожатие SSE | Запись в лог |
update | Любое изменение .arch или манифеста | Перезапрос модели и CSS виджетов |
widgets-changed | Изменилось время модификации widgets.js | Полная перезагрузка страницы |
Каналу widgets-changed требуется полная перезагрузка, потому что customElements.define() срабатывает однократно на тег — повторный импорт того же модуля в браузере ничего не делает.
Расположение файлов
my-project/├── package.archspace # name, widgets path, deps, uses├── widgets.js # customElements.define(...) for tags used in widget:└── src/ ├── orders.arch └── kinds.archДиагностические коды
| Код | Значение |
|---|---|
WIDGETS_FILE_NOT_FOUND | Поле widgets: указывает на отсутствующий файл |
KIND_NOT_VISIBLE_IN_FILE | .arch ссылается на вид, импортированный только в другом файле |
USE_TYPE_NOT_EXPORTED | Прямой импорт нацелен на тип без модификатора export |
Компромиссы на одном экране
| Что нужно | widget: <tag> | widget: "<html>" |
|---|---|---|
| Статическая карточка-идентификатор | избыточно | идеально |
| Состояния hover / focus / active | ✅ | ✅ на чистом CSS |
| Утилиты Tailwind | ✅ в widgets.js | ✅ компилируются на сервере |
| Полный DOM-инструментарий (canvas, WebGL) | ✅ | ❌ |
| Отрисовка с учётом связей | ✅ через el.archConnections | ❌ |
| Живые обновления из внешних данных | ✅ | ❌ |
| Ноль JS-файлов в проекте | ❌ | ✅ |
Берите форму с тегом, когда нужна настоящая интерактивность или структурированные данные; берите форму с шаблоном, когда хотите, чтобы визуализация декларативно следовала из модели.
Резюме
- Виджет определяет, как отрисовывается узел, объявляется через поле
widget:. - Две формы: пользовательский элемент (значение-идентификатор, на JS) или встроенный шаблон (строковое значение, HTML).
- Пользовательские элементы получают модель и состояние области просмотра через атрибуты, CSS-переменные и DOM-свойства.
- Встроенные шаблоны подставляют значения по
{{path}}со строгими правилами без логики. - Шаблоны используют UnoCSS, скомпилированный на сервере, — утилиты Tailwind работают из коробки.
widgetиwidget.<...>— обычные поля, поэтому к ним применимыcascade, переопределение и отбрасывание.- Горячая перезагрузка управляется событиями; изменения скрипта виджетов требуют полной перезагрузки страницы.
Что дальше
Глава 21: CLI → — открывает Часть V, в которой рассматривается инструментарий вокруг языка: CLI, интеграция с редакторами, встраивание диаграмм и API библиотек.