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

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.iconicon="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-compact0→1 по мере входа узла в полосу «compact-readable»
CSS-переменная --arch-band-full0→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> по умолчанию на каждый объявляемый вид — servicearch-service, databasearch-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 библиотек.