Compare commits
8
Commits
d51d8bb7f6
...
1bdfc6c240
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bdfc6c240 | ||
|
|
70ce00f2d0 | ||
|
|
999864e5b0 | ||
|
|
3adeb33b1b | ||
|
|
be6463fd59 | ||
|
|
47d4f19d99 | ||
|
|
17e150b1c7 | ||
|
|
8a79dfe84d |
@@ -3,6 +3,8 @@ import { applyGlassMaterial, applyNodedcTheme, defaultGlassMaterial, type GlassM
|
|||||||
import { createTemplateFeatures, getPageTemplate, pageTemplates, type PageTemplateDefinition } from "@nodedc/page-patterns";
|
import { createTemplateFeatures, getPageTemplate, pageTemplates, type PageTemplateDefinition } from "@nodedc/page-patterns";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
StatusBadge,
|
||||||
|
ProgressBar,
|
||||||
AdminNavigationPanel,
|
AdminNavigationPanel,
|
||||||
AppHeader,
|
AppHeader,
|
||||||
ApplicationPanel,
|
ApplicationPanel,
|
||||||
@@ -29,6 +31,8 @@ import {
|
|||||||
InspectorSelectField,
|
InspectorSelectField,
|
||||||
MediaSourceField,
|
MediaSourceField,
|
||||||
RangeControl,
|
RangeControl,
|
||||||
|
ResourceRow,
|
||||||
|
ResourceList,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
Select,
|
Select,
|
||||||
ShareAccessModal,
|
ShareAccessModal,
|
||||||
@@ -1408,6 +1412,20 @@ export function CatalogApp() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="catalog-preview__explanation">При reduced motion кольцо остаётся видимым без вращения; процесс и его завершение принадлежат приложению.</p>
|
<p className="catalog-preview__explanation">При reduced motion кольцо остаётся видимым без вращения; процесс и его завершение принадлежат приложению.</p>
|
||||||
</Preview>
|
</Preview>
|
||||||
|
<Preview title="Лампы состояния" note="один индикатор без дублирующего значка"><StatusBadge variant="indicator" tone="success" aria-label="Готово" title="Готово" /><StatusBadge variant="indicator" aria-label="Недоступно" title="Недоступно" /></Preview>
|
||||||
|
<Preview title="Линейный прогресс" note="измеренный / неизвестный / завершённый">
|
||||||
|
<ProgressBar label="Подготовка" value={0.6} valueText="Три этапа из пяти" />
|
||||||
|
<ProgressBar label="Получение сведений" />
|
||||||
|
<ProgressBar label="Завершено" value={1} />
|
||||||
|
</Preview>
|
||||||
|
<Preview title="Строки ресурсов" note="Mission Core / общий список">
|
||||||
|
<ResourceList aria-label="Пример списка ресурсов">
|
||||||
|
<li><ResourceRow icon={<Icon name="file" />} title="Сохранённый результат" metadata="Сегодня · доступен для просмотра" actions={<IconButton label="Просмотреть пример" onClick={() => setProjectName("Сохранённый результат")}><Icon name="eye" /></IconButton>} /></li>
|
||||||
|
<li><ResourceRow icon={<Icon name="camera" />} title="Подключённая камера" description="Индикатор перед названием" statusPlacement="leading" status={<StatusBadge variant="indicator" tone="success" aria-label="Подключено" title="Подключено" />} actions={<IconButton label="Просмотреть камеру" onClick={() => setProjectName("Подключённая камера")}><Icon name="eye" /></IconButton>} /></li>
|
||||||
|
<li><ResourceRow icon={<Icon name="camera" />} title="Подготовка камеры" metadata="Проверка потоков" progress={{label:"Подготовка камеры",value:0.8}} aria-busy="true" actions={<IconButton label="Просмотр недоступен" disabled><Icon name="eye" /></IconButton>} /></li>
|
||||||
|
</ResourceList>
|
||||||
|
<SettingsCard title="Устройства не обнаружены" description="Подключите устройство, чтобы оно появилось в списке." />
|
||||||
|
</Preview>
|
||||||
<Preview title="Оконные действия" note="круг `46 px`" className="catalog-preview--compact">
|
<Preview title="Оконные действия" note="круг `46 px`" className="catalog-preview--compact">
|
||||||
<div className="catalog-window-actions-demo">
|
<div className="catalog-window-actions-demo">
|
||||||
<Button shape="pill" icon={<Icon name="refresh" />}>Обновить источник</Button>
|
<Button shape="pill" icon={<Icon name="refresh" />}>Обновить источник</Button>
|
||||||
@@ -1992,6 +2010,7 @@ export function CatalogApp() {
|
|||||||
endPanel={<div ref={setMapSettingsPanelHost} className="catalog-map-settings-panel-host" />}
|
endPanel={<div ref={setMapSettingsPanelHost} className="catalog-map-settings-panel-host" />}
|
||||||
header={
|
header={
|
||||||
<AppHeader
|
<AppHeader
|
||||||
|
brandMonochrome
|
||||||
brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />}
|
brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />}
|
||||||
brandHref="/"
|
brandHref="/"
|
||||||
center={
|
center={
|
||||||
|
|||||||
@@ -44,3 +44,7 @@ Modal остаётся portal-слоем. Его тело прокручивае
|
|||||||
- предметное содержимое content window.
|
- предметное содержимое content window.
|
||||||
|
|
||||||
Shell владеет геометрией, слоями, переходами и breakpoint-поведением. См. готовый путь подключения в `docs/CONSUMPTION.md`.
|
Shell владеет геометрией, слоями, переходами и breakpoint-поведением. См. готовый путь подключения в `docs/CONSUMPTION.md`.
|
||||||
|
|
||||||
|
На узком экране скрывается именно действие `data-action="expand"`.
|
||||||
|
Порядок utility actions не является признаком типа действия: плюс и обновление
|
||||||
|
остаются доступны, даже когда стоят первыми в шапке. Исправлено при QA Node 0.3.
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ Window — единая механика открытия modal и правой
|
|||||||
|
|
||||||
Содержание, сохранение и запросы принадлежат приложению.
|
Содержание, сохранение и запросы принадлежат приложению.
|
||||||
|
|
||||||
|
Modeless Inspector закрывается независимо от результата автосохранения. Приложение сначала закрывает controlled `Window`, затем завершает сохранение асинхронно; сбой сообщает через компактный `ToastStack` с tone `error`, сохраняя draft текущего просмотра. Нельзя удерживать окно открытым, выводить внутри него browser-default `p`/heading или увеличивать текст ошибки до title-размера.
|
||||||
|
|
||||||
Для modeless Inspector доступен `draggable`: окно двигается за header, не блокирует приложение и ограничивается viewport. Modal-окна не становятся draggable автоматически.
|
Для modeless Inspector доступен `draggable`: окно двигается за header, не блокирует приложение и ограничивается viewport. Modal-окна не становятся draggable автоматически.
|
||||||
|
|
||||||
## WorkspaceWindow
|
## WorkspaceWindow
|
||||||
@@ -158,6 +160,8 @@ ConfirmationModal оборачивает Window и защищает async-confir
|
|||||||
|
|
||||||
`SplitPane` — контролируемая раскладка двух синхронных представлений с общей регулируемой границей. Приложение передаёт содержимое обеих панелей, процент primary-панели и callback изменения; дизайн-система владеет pointer capture, ограничениями размеров, focus-state и доступностью `role="separator"`.
|
`SplitPane` — контролируемая раскладка двух синхронных представлений с общей регулируемой границей. Приложение передаёт содержимое обеих панелей, процент primary-панели и callback изменения; дизайн-система владеет pointer capture, ограничениями размеров, focus-state и доступностью `role="separator"`.
|
||||||
|
|
||||||
|
Локальные toolbar и overlay каждого viewport являются содержимым соответствующей панели. Их нельзя позиционировать от общего workspace или вычислять по cursor/event-эвристике вложенного renderer. Если renderer хранит свою раскладку, приложение передаёт ему тот же controlled процент из `SplitPane`, чтобы renderer и локальный UI использовали одну геометрию.
|
||||||
|
|
||||||
Вертикальная граница меняется мышью или клавишами `←/→`, горизонтальная — `↑/↓`; `Home/End` переходят к разрешённым границам, а `Shift` ускоряет клавиатурный шаг. Доля сохраняется при resize контейнера. Минимальные проценты обеих панелей применяются одинаково к pointer и keyboard interaction.
|
Вертикальная граница меняется мышью или клавишами `←/→`, горизонтальная — `↑/↓`; `Home/End` переходят к разрешённым границам, а `Shift` ускоряет клавиатурный шаг. Доля сохраняется при resize контейнера. Минимальные проценты обеих панелей применяются одинаково к pointer и keyboard interaction.
|
||||||
|
|
||||||
Разделитель является структурной границей между двумя рабочими представлениями и не создаёт декоративную рамку вокруг контента. При `resizable={false}` панели сохраняют ту же стабильную DOM-композицию, а separator полностью отсутствует в визуальном слое и accessibility tree; это позволяет приложению сворачивать одну из панелей без remount тяжёлого renderer.
|
Разделитель является структурной границей между двумя рабочими представлениями и не создаёт декоративную рамку вокруг контента. При `resizable={false}` панели сохраняют ту же стабильную DOM-композицию, а separator полностью отсутствует в визуальном слое и accessibility tree; это позволяет приложению сворачивать одну из панелей без remount тяжёлого renderer.
|
||||||
@@ -312,3 +316,23 @@ Inspector владеет:
|
|||||||
Для Engine Environment Settings desktop-контракт точный: окно `390 px`, рабочая колонка `330 px`, строка `154 + 14 + 162 px`, высота контрола `46 px`, section header `50 px` с радиусом `12 px`. Accent-filled section headers и полноширинные range/checker/select сохраняют геометрию исходника.
|
Для Engine Environment Settings desktop-контракт точный: окно `390 px`, рабочая колонка `330 px`, строка `154 + 14 + 162 px`, высота контрола `46 px`, section header `50 px` с радиусом `12 px`. Accent-filled section headers и полноширинные range/checker/select сохраняют геометрию исходника.
|
||||||
|
|
||||||
Engine продолжает владеть определениями полей, типами нод и сохранением значений. В живом catalog эти collapsible sections находятся в `Guideline → Контролы`; отдельной product-вкладки Inspector нет.
|
Engine продолжает владеть определениями полей, типами нод и сохранением значений. В живом catalog эти collapsible sections находятся в `Guideline → Контролы`; отдельной product-вкладки Inspector нет.
|
||||||
|
|
||||||
|
## ResourceRow и ResourceList
|
||||||
|
|
||||||
|
Общие компактные строки ресурсов, выделенные из Mission Core AI Inference.
|
||||||
|
Контракт, состояния и происхождение: [RESOURCE_ROW.md](RESOURCE_ROW.md).
|
||||||
|
|
||||||
|
Светлый одноцветный знак шапки явно помечается `AppHeader.brandMonochrome` и
|
||||||
|
`HeaderWorkspace.monochrome`: общая тема обеспечивает контраст на светлом фоне.
|
||||||
|
По умолчанию флаг выключен; цветные изображения и аватары не меняются.
|
||||||
|
|
||||||
|
## ProgressBar
|
||||||
|
|
||||||
|
Линейная полоса выполнения: `label`, измеренная доля `value` 0…1 или
|
||||||
|
неопределённая продолжительность без `value`, доступный `valueText`.
|
||||||
|
В `ResourceRow` передаётся через `progress` и заменяет статус между
|
||||||
|
наименованием и кнопками. Процент принадлежит приложению, анимация его не выдумывает.
|
||||||
|
См. [контракт](PROGRESS_BAR.md).
|
||||||
|
|
||||||
|
StatusBadge `variant="indicator"` отображает только одну лампу состояния.
|
||||||
|
Передавайте `aria-label` и `title`; дополнительный текст и значки скрываются.
|
||||||
|
|||||||
+6
-1
@@ -29,6 +29,12 @@
|
|||||||
|
|
||||||
Допустимы density variants, если они имеют устойчивое назначение (`default`, `compact`) и тестируются как часть API.
|
Допустимы density variants, если они имеют устойчивое назначение (`default`, `compact`) и тестируются как часть API.
|
||||||
|
|
||||||
|
## Типографика и операционные сообщения
|
||||||
|
|
||||||
|
Продуктовый интерфейс использует только типографические токены. Browser-default размеры у `h1`–`h6`, `p`, `strong` и form controls не являются допустимым стилем. `--nodedc-font-size-title` разрешён только для заголовка страницы или окна; названия конфигураций, статусы, ошибки, подписи полей и сообщения внутри рабочей сцены используют `md`, `sm`, `xs` или готовую типографику канонического компонента.
|
||||||
|
|
||||||
|
Ошибка сохранения или запроса не рендерится свободным текстовым блоком поверх рабочей сцены. Приложение использует семантический error-state канонического поля либо `ToastStack`, который владеет размером текста, цветом, положением и закрытием. Ошибка автосохранения modeless-инспектора не блокирует закрытие окна: draft остаётся в текущем просмотре, окно закрывается сразу, а сбой показывается неблокирующим error toast.
|
||||||
|
|
||||||
## Deprecated
|
## Deprecated
|
||||||
|
|
||||||
Перед удалением export:
|
Перед удалением export:
|
||||||
@@ -53,4 +59,3 @@
|
|||||||
## Владение
|
## Владение
|
||||||
|
|
||||||
У дизайн-системы должен быть явный code owner. Product team может предлагать компоненты, но общий API и theme contract проходят отдельное review, потому что изменение распространяется на все будущие приложения.
|
У дизайн-системы должен быть явный code owner. Product team может предлагать компоненты, но общий API и theme contract проходят отдельное review, потому что изменение распространяется на все будущие приложения.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Компактная операционная типографика
|
||||||
|
|
||||||
|
Уточнение владельца 06.09.2026 для Mission Core / Node, обязательное для
|
||||||
|
общих компонентов NODE.DC. Повторяющиеся сообщения и подписи не оформляются
|
||||||
|
крупными заголовками. Этот документ зарегистрирован в registry/registry.json.
|
||||||
|
|
||||||
|
- Заголовок страницы или окна: только предусмотренный шаблоном title-токен.
|
||||||
|
- Заголовок секции, SettingsCard, короткое сообщение пустого списка: md
|
||||||
|
(0.8125rem / 13 px при стандартном корневом размере), без увеличения через h2.
|
||||||
|
- Описание, пояснение, сведения о состоянии: sm (0.75rem / 12 px).
|
||||||
|
- Технические метаданные: xs либо типографика ResourceRow.
|
||||||
|
- Browser-default h1–h6/p/strong и локальные крупные размеры запрещены.
|
||||||
|
- Размер не умножается из-за вложенности карточек, ошибки или отсутствия данных.
|
||||||
|
|
||||||
|
SettingsCard и nodedc-empty-state закрепляют эти размеры в ui-core. Текст
|
||||||
|
«Устройства не обнаружены» не является заголовком нового раздела. В живом
|
||||||
|
каталоге рядом со строками ресурсов есть пример пустого списка.
|
||||||
|
|
||||||
|
В завершённом этапе настройки окружения status-слот ResourceRow содержит
|
||||||
|
только Icon check с доступным названием. Галочка не оборачивается в StatusBadge,
|
||||||
|
Checker, круг, pill или кнопку. Это знак результата, а не интерактивный контрол.
|
||||||
|
Лампочка состояния устройства отдельно использует StatusBadge indicator.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# ProgressBar
|
||||||
|
|
||||||
|
Владелец Mission Core 05.09.2026 явно запросил горизонтальную заполняющуюся
|
||||||
|
полосу вместо кружка и точки в строке подготовки устройства. Общий компонент
|
||||||
|
добавлен в Design Guideline до подключения в Node/Core.
|
||||||
|
|
||||||
|
`value` — завершённая доля 0…1; NaN/Infinity трактуются как неизвестная величина,
|
||||||
|
выход за границы ограничивается. Без value используется движущийся сегмент,
|
||||||
|
aria-valuenow отсутствует. `label` обязателен; `valueText` описывает текущий этап.
|
||||||
|
Нет интерактивности и focus. Темы используют существующие семантические токены.
|
||||||
|
При reduced motion движение выключено, видимый сегмент сохраняется.
|
||||||
|
|
||||||
|
ResourceRow.progress занимает свободное место между текстом и actions, скрывает
|
||||||
|
status, на узком экране переносится ниже текста. Готовность не вычисляется
|
||||||
|
компонентом. Каталог содержит завершённый, определённый и неизвестный прогресс.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# ResourceRow и ResourceList
|
||||||
|
|
||||||
|
Владелец 2026-09-05 запросил перенос существующих длинных строк Mission Core
|
||||||
|
AI Inference в Node с повторным использованием Design Guideline. Источник:
|
||||||
|
ObservatoryWorkspace / observatory-evidence-card. Это выделение существующего
|
||||||
|
оформления в общий компонент, без лабораторной модели данных.
|
||||||
|
|
||||||
|
`ResourceList` содержит обычные `li`; внутри — `ResourceRow` с обязательным
|
||||||
|
`title`, опциональными `icon`, `description`, `metadata`, `status`, `actions`.
|
||||||
|
Слоты действий принимают канонические Button/IconButton. Клик по всей строке
|
||||||
|
не назначается: самостоятельные действия имеют свои доступные подписи и focus.
|
||||||
|
|
||||||
|
Геометрия источника сохранена: минимум 4.25rem, отступы .7/.85rem, значок
|
||||||
|
2.15rem, компактный радиус и существующие токены поверхности/типографики.
|
||||||
|
Длинное название переносится; метаданные сокращаются визуально, полный текст
|
||||||
|
можно передать через title. На узком экране статус и действия переходят ниже.
|
||||||
|
|
||||||
|
Состояния поставляет consumer: нормальное, пустой список, загрузка, ошибка,
|
||||||
|
недоступное действие. Pending отмечается aria-busy и ActivityIndicator;
|
||||||
|
disabled и keyboard/focus остаются у общего контрола. Строка не объявляет
|
||||||
|
объект доступным или подключённым самостоятельно.
|
||||||
|
|
||||||
|
Пример находится в живом каталоге, раздел «Контролы → Строки ресурсов».
|
||||||
|
|
||||||
|
Для длительных операций с линейной индикацией используйте `progress`
|
||||||
|
(ProgressBar: label, value, valueText). Он заменяет status, оставляет действия
|
||||||
|
справа и занимает промежуток после названия.
|
||||||
|
|
||||||
|
`statusPlacement="leading"` размещает компактный индикатор после иконки и
|
||||||
|
перед всем текстовым блоком, с выравниванием по вертикальному центру строки.
|
||||||
|
По умолчанию остаётся `trailing` перед действиями. Статус не дублируется;
|
||||||
|
во время `progress` он скрыт в любом положении. Для одной лампы используйте
|
||||||
|
StatusBadge variant="indicator" с aria-label и title. Это согласованное
|
||||||
|
06.09.2026 расположение индикатора устройств Mission Core / Node.
|
||||||
@@ -585,13 +585,13 @@ textarea.nodedc-field__control {
|
|||||||
|
|
||||||
.nodedc-settings-card__titles h2 {
|
.nodedc-settings-card__titles h2 {
|
||||||
color: var(--nodedc-text-primary);
|
color: var(--nodedc-text-primary);
|
||||||
font-size: 1rem;
|
font-size: var(--nodedc-font-size-md);
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-settings-card__titles p {
|
.nodedc-settings-card__titles p {
|
||||||
color: var(--nodedc-text-muted);
|
color: var(--nodedc-text-muted);
|
||||||
font-size: 0.75rem;
|
font-size: var(--nodedc-font-size-sm);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3569,6 +3569,7 @@ textarea.nodedc-field__control {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-empty-state {
|
.nodedc-empty-state {
|
||||||
|
font-size: var(--nodedc-font-size-sm);
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 12rem;
|
min-height: 12rem;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -3580,7 +3581,7 @@ textarea.nodedc-field__control {
|
|||||||
|
|
||||||
.nodedc-empty-state strong {
|
.nodedc-empty-state strong {
|
||||||
color: var(--nodedc-text-primary);
|
color: var(--nodedc-text-primary);
|
||||||
font-size: var(--nodedc-font-size-lg);
|
font-size: var(--nodedc-font-size-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
@@ -3680,7 +3681,7 @@ textarea.nodedc-field__control {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-application-panel__action:first-child {
|
.nodedc-application-panel__action[data-action="expand"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3777,3 +3778,46 @@ textarea.nodedc-field__control {
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ResourceRow: admitted extraction of Mission Core AI Inference list geometry. */
|
||||||
|
.nodedc-resource-list { display: grid; gap: .4rem; margin: 0; padding: 0; list-style: none; }
|
||||||
|
.nodedc-resource-row { display: flex; align-items: center; gap: .8rem; min-height: 4.25rem; padding: .7rem .85rem; border-radius: var(--nodedc-radius-control-compact); background: var(--nodedc-glass-control-bg); }
|
||||||
|
.nodedc-resource-row__icon { display: grid; flex: 0 0 2.15rem; height: 2.15rem; place-items: center; border-radius: var(--nodedc-radius-circle); background: var(--nodedc-panel-icon-bg); color: var(--nodedc-text-secondary); }
|
||||||
|
.nodedc-resource-row__copy { display: grid; flex: 1; min-width: 0; gap: .16rem; }
|
||||||
|
.nodedc-resource-row__copy strong { font-size: var(--nodedc-font-size-sm); line-height: 1.3; overflow-wrap: anywhere; }
|
||||||
|
.nodedc-resource-row__copy > span, .nodedc-resource-row__copy > small { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.nodedc-resource-row__copy > span { color: var(--nodedc-text-secondary); font-size: var(--nodedc-font-size-sm); }
|
||||||
|
.nodedc-resource-row__copy > small { color: var(--nodedc-text-muted); font-size: var(--nodedc-font-size-xs); }
|
||||||
|
.nodedc-resource-row__actions { display: flex; align-items: center; gap: .85rem; }
|
||||||
|
.nodedc-resource-row__status { font-size: var(--nodedc-font-size-sm); }
|
||||||
|
.nodedc-resource-row--leading-status > .nodedc-resource-row__status { display: flex; align-items: center; flex: 0 0 auto; }
|
||||||
|
@media (max-width: 540px) {
|
||||||
|
.nodedc-resource-row { flex-wrap: wrap; }
|
||||||
|
.nodedc-resource-row__copy { flex-basis: calc(100% - 3rem); }
|
||||||
|
.nodedc-resource-row--leading-status > .nodedc-resource-row__copy { flex: 1 1 0; }
|
||||||
|
.nodedc-resource-row__actions { margin-left: auto; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-nodedc-theme="light"] :is(.nodedc-header__brand, .nodedc-header__workspace)[data-monochrome="true"] img { filter: brightness(0); }
|
||||||
|
|
||||||
|
/* Owner-admitted linear progress in the shared resource row. */
|
||||||
|
.nodedc-progress-bar { min-width: 3rem; height: var(--nodedc-space-2); overflow: hidden; border-radius: var(--nodedc-radius-circle); background: var(--nodedc-glass-control-bg); }
|
||||||
|
.nodedc-progress-bar__fill { display: block; width: 100%; height: 100%; background: var(--nodedc-text-secondary); border-radius: inherit; transform: scaleX(var(--nodedc-progress-fraction)); transform-origin: left; transition: transform var(--nodedc-duration-normal) var(--nodedc-ease-standard); }
|
||||||
|
.nodedc-progress-bar[data-indeterminate] > .nodedc-progress-bar__fill { width: 35%; animation: nodedc-progress-travel 1.5s ease-in-out infinite alternate; }
|
||||||
|
@keyframes nodedc-progress-travel { from { transform: translateX(-100%); } to { transform: translateX(285%); } }
|
||||||
|
.nodedc-resource-row--progress > .nodedc-resource-row__copy { flex: 0 1 35%; }
|
||||||
|
.nodedc-resource-row--progress > .nodedc-progress-bar { flex: 1; }
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
.nodedc-resource-row--progress > .nodedc-resource-row__copy { flex-basis: calc(100% - 3rem); }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.nodedc-progress-bar__fill { transition: none; }
|
||||||
|
.nodedc-progress-bar[data-indeterminate] > .nodedc-progress-bar__fill { animation: none; width: 100%; transform: scaleX(0.5); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-status[data-variant="indicator"] { min-height: 0; width: .42rem; height: .42rem; padding: 0; gap: 0; background: transparent; }
|
||||||
|
.nodedc-status[data-variant="indicator"]::before { content: ""; display: block; width: .42rem; height: .42rem; flex: 0 0 .42rem; border-radius: var(--nodedc-radius-circle); background: var(--nodedc-text-muted); }
|
||||||
|
.nodedc-status[data-variant="indicator"][data-tone="success"]::before { background: rgb(var(--nodedc-success-rgb)); }
|
||||||
|
.nodedc-status[data-variant="indicator"][data-tone="warning"]::before { background: rgb(var(--nodedc-warning-rgb)); }
|
||||||
|
.nodedc-status[data-variant="indicator"][data-tone="danger"]::before { background: rgb(var(--nodedc-danger-rgb)); }
|
||||||
|
.nodedc-status[data-variant="indicator"][data-tone="accent"]::before { background: rgb(var(--nodedc-accent-rgb)); }
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export interface AppHeaderProps {
|
|||||||
brand: ReactNode;
|
brand: ReactNode;
|
||||||
brandHref?: string;
|
brandHref?: string;
|
||||||
brandLabel?: string;
|
brandLabel?: string;
|
||||||
|
/** Opt in only for a light, single-color mark, never a full-color image. */
|
||||||
|
brandMonochrome?: boolean;
|
||||||
left?: ReactNode;
|
left?: ReactNode;
|
||||||
center?: ReactNode;
|
center?: ReactNode;
|
||||||
right?: ReactNode;
|
right?: ReactNode;
|
||||||
@@ -13,14 +15,15 @@ export function AppHeader({
|
|||||||
brand,
|
brand,
|
||||||
brandHref,
|
brandHref,
|
||||||
brandLabel = "NODE.DC",
|
brandLabel = "NODE.DC",
|
||||||
|
brandMonochrome = false,
|
||||||
left,
|
left,
|
||||||
center,
|
center,
|
||||||
right,
|
right,
|
||||||
}: AppHeaderProps) {
|
}: AppHeaderProps) {
|
||||||
const brandNode = brandHref ? (
|
const brandNode = brandHref ? (
|
||||||
<a className="nodedc-header__brand" href={brandHref} aria-label={brandLabel}>{brand}</a>
|
<a className="nodedc-header__brand" data-monochrome={brandMonochrome || undefined} href={brandHref} aria-label={brandLabel}>{brand}</a>
|
||||||
) : (
|
) : (
|
||||||
<span className="nodedc-header__brand" aria-label={brandLabel}>{brand}</span>
|
<span className="nodedc-header__brand" data-monochrome={brandMonochrome || undefined} aria-label={brandLabel}>{brand}</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -100,11 +103,12 @@ export interface HeaderWorkspaceProps {
|
|||||||
label: string;
|
label: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
kind?: "mark" | "avatar";
|
kind?: "mark" | "avatar";
|
||||||
|
monochrome?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeaderWorkspace({ label, imageUrl, kind = "mark" }: HeaderWorkspaceProps) {
|
export function HeaderWorkspace({ label, imageUrl, kind = "mark", monochrome = false }: HeaderWorkspaceProps) {
|
||||||
return (
|
return (
|
||||||
<span className="nodedc-header__workspace" data-kind={kind} title={label}>
|
<span className="nodedc-header__workspace" data-kind={kind} data-monochrome={monochrome || undefined} title={label}>
|
||||||
{imageUrl ? <img src={imageUrl} alt="" /> : label.slice(0, 2).toUpperCase()}
|
{imageUrl ? <img src={imageUrl} alt="" /> : label.slice(0, 2).toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { CSSProperties, HTMLAttributes } from "react";
|
||||||
|
import { cn } from "./cn.js";
|
||||||
|
|
||||||
|
export interface ProgressBarProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
||||||
|
label: string;
|
||||||
|
/** Completed fraction from 0 to 1. Omit when the amount is unknown. */
|
||||||
|
value?: number;
|
||||||
|
valueText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProgressBar({ label, value, valueText, className, style, ...props }: ProgressBarProps) {
|
||||||
|
const fraction = typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : undefined;
|
||||||
|
return <div {...props} className={cn("nodedc-progress-bar", className)} role="progressbar"
|
||||||
|
aria-label={label} aria-valuemin={0} aria-valuemax={100}
|
||||||
|
aria-valuenow={fraction === undefined ? undefined : Math.round(fraction * 100)}
|
||||||
|
aria-valuetext={valueText} data-indeterminate={fraction === undefined || undefined}
|
||||||
|
style={{ ...style, "--nodedc-progress-fraction": fraction ?? 0 } as CSSProperties}>
|
||||||
|
<span className="nodedc-progress-bar__fill" />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { HTMLAttributes, ReactNode } from "react";
|
||||||
|
import { ProgressBar, type ProgressBarProps } from "./ProgressBar.js";
|
||||||
|
import { cn } from "./cn.js";
|
||||||
|
|
||||||
|
export interface ResourceRowProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
||||||
|
title: ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
description?: ReactNode;
|
||||||
|
metadata?: ReactNode;
|
||||||
|
status?: ReactNode;
|
||||||
|
statusPlacement?: "leading" | "trailing";
|
||||||
|
actions?: ReactNode;
|
||||||
|
progress?: Pick<ProgressBarProps, "label" | "value" | "valueText">;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compact resource presentation extracted from Mission Core AI Inference.
|
||||||
|
* Actions stay canonical controls; the row itself is not a nested button. */
|
||||||
|
export function ResourceRow({ title, icon, description, metadata, status, statusPlacement = "trailing", actions, progress, className, ...props }: ResourceRowProps) {
|
||||||
|
const leadingStatus = !progress && !!status && statusPlacement === "leading";
|
||||||
|
return <div className={cn("nodedc-resource-row", progress && "nodedc-resource-row--progress", leadingStatus && "nodedc-resource-row--leading-status", className)} {...props}>
|
||||||
|
{icon ? <span className="nodedc-resource-row__icon" aria-hidden="true">{icon}</span> : null}
|
||||||
|
{leadingStatus ? <div className="nodedc-resource-row__status">{status}</div> : null}
|
||||||
|
<div className="nodedc-resource-row__copy">
|
||||||
|
<strong>{title}</strong>
|
||||||
|
{description ? <span>{description}</span> : null}
|
||||||
|
{metadata ? <small>{metadata}</small> : null}
|
||||||
|
</div>
|
||||||
|
{progress ? <ProgressBar {...progress} /> : null}
|
||||||
|
{!progress && status && statusPlacement === "trailing" ? <div className="nodedc-resource-row__status">{status}</div> : null}
|
||||||
|
{actions ? <div className="nodedc-resource-row__actions">{actions}</div> : null}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResourceList({ className, children, ...props }: HTMLAttributes<HTMLUListElement>) {
|
||||||
|
return <ul className={cn("nodedc-resource-list", className)} {...props}>{children}</ul>;
|
||||||
|
}
|
||||||
@@ -5,12 +5,13 @@ export type StatusTone = "neutral" | "success" | "warning" | "danger" | "accent"
|
|||||||
|
|
||||||
export interface StatusBadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
export interface StatusBadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||||
tone?: StatusTone;
|
tone?: StatusTone;
|
||||||
|
variant?: "badge" | "indicator";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusBadge({ tone = "neutral", className, children, ...props }: StatusBadgeProps) {
|
export function StatusBadge({ tone = "neutral", variant = "badge", className, children, ...props }: StatusBadgeProps) {
|
||||||
return (
|
return (
|
||||||
<span className={cn("nodedc-status", className)} data-tone={tone === "neutral" ? undefined : tone} {...props}>
|
<span className={cn("nodedc-status", className)} data-tone={tone === "neutral" ? undefined : tone} data-variant={variant === "indicator" ? variant : undefined} {...props}>
|
||||||
{children}
|
{variant === "indicator" ? null : children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export * from "./Inspector.js";
|
|||||||
export * from "./Icon.js";
|
export * from "./Icon.js";
|
||||||
export * from "./MediaSourceField.js";
|
export * from "./MediaSourceField.js";
|
||||||
export * from "./RangeControl.js";
|
export * from "./RangeControl.js";
|
||||||
|
export * from "./ResourceRow.js";
|
||||||
export * from "./SegmentedControl.js";
|
export * from "./SegmentedControl.js";
|
||||||
export * from "./Select.js";
|
export * from "./Select.js";
|
||||||
export * from "./StatusBadge.js";
|
export * from "./StatusBadge.js";
|
||||||
@@ -27,3 +28,5 @@ export * from "./Toast.js";
|
|||||||
export * from "./UserProfileMenu.js";
|
export * from "./UserProfileMenu.js";
|
||||||
export * from "./Window.js";
|
export * from "./Window.js";
|
||||||
export * from "./WorkspaceWindow.js";
|
export * from "./WorkspaceWindow.js";
|
||||||
|
|
||||||
|
export { ProgressBar, type ProgressBarProps } from "./ProgressBar.js";
|
||||||
|
|||||||
@@ -1,6 +1,53 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": "1.0.0",
|
"schemaVersion": "1.0.0",
|
||||||
"components": [
|
"components": [
|
||||||
|
{
|
||||||
|
"id": "progress-bar",
|
||||||
|
"status": "baseline",
|
||||||
|
"package": "@nodedc/ui-react",
|
||||||
|
"exports": [
|
||||||
|
"ProgressBar",
|
||||||
|
"ProgressBarProps"
|
||||||
|
],
|
||||||
|
"domContract": [
|
||||||
|
"nodedc-progress-bar"
|
||||||
|
],
|
||||||
|
"summary": "Accessible determinate or indeterminate linear progress, including the central ResourceRow slot.",
|
||||||
|
"variants": [
|
||||||
|
"determinate",
|
||||||
|
"indeterminate"
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
"Consumer supplies measured progress; elapsed time must not invent completion.",
|
||||||
|
"ResourceRow progress replaces status and occupies space between copy and actions.",
|
||||||
|
"Reduced motion preserves a visible non-animated indicator."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "resource-row",
|
||||||
|
"status": "baseline",
|
||||||
|
"package": "@nodedc/ui-react",
|
||||||
|
"exports": [
|
||||||
|
"ResourceRow",
|
||||||
|
"ResourceList"
|
||||||
|
],
|
||||||
|
"domContract": [
|
||||||
|
"nodedc-resource-row",
|
||||||
|
"nodedc-resource-list"
|
||||||
|
],
|
||||||
|
"summary": "Mission Core AI Inference compact rows extracted for shared resource lists.",
|
||||||
|
"behavior": [
|
||||||
|
"wrapping title",
|
||||||
|
"optional metadata and status",
|
||||||
|
"optional leading status between icon and copy, vertically centered",
|
||||||
|
"canonical action controls",
|
||||||
|
"responsive action placement"
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
"The consumer supplies identity and states; the row owns presentation only.",
|
||||||
|
"Use ordinary list items and canonical buttons; do not nest actions inside a clickable row."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "glass-surface",
|
"id": "glass-surface",
|
||||||
"status": "baseline",
|
"status": "baseline",
|
||||||
@@ -160,7 +207,9 @@
|
|||||||
"Open state belongs to the application; focus/layer behavior belongs to the component.",
|
"Open state belongs to the application; focus/layer behavior belongs to the component.",
|
||||||
"The same window contract is used by Launcher, CMS and SEO; theme variables supply color differences.",
|
"The same window contract is used by Launcher, CMS and SEO; theme variables supply color differences.",
|
||||||
"A right-side inspector is modeless by default: no dimming, no backdrop blur, no backdrop close, no focus trap and no body scroll lock.",
|
"A right-side inspector is modeless by default: no dimming, no backdrop blur, no backdrop close, no focus trap and no body scroll lock.",
|
||||||
"A draggable inspector moves by its header and remains clamped to the viewport."
|
"A draggable inspector moves by its header and remains clamped to the viewport.",
|
||||||
|
"Closing a modeless inspector is never gated by autosave; preserve the draft, close immediately and report persistence failure through ToastStack.",
|
||||||
|
"Operational copy inside Window uses component typography tokens; browser-default paragraph or heading sizes are forbidden."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -200,7 +249,8 @@
|
|||||||
"rules": [
|
"rules": [
|
||||||
"The application owns the controlled primary percentage and panel content; the component owns separator interaction and accessibility.",
|
"The application owns the controlled primary percentage and panel content; the component owns separator interaction and accessibility.",
|
||||||
"Minimum primary and secondary percentages clamp both pointer and keyboard changes.",
|
"Minimum primary and secondary percentages clamp both pointer and keyboard changes.",
|
||||||
"The separator is a necessary structural divider, never a decorative perimeter border."
|
"The separator is a necessary structural divider, never a decorative perimeter border.",
|
||||||
|
"Viewport-local toolbars belong to their SplitPane panel, and embedded renderers receive the same controlled percentage instead of exposing cursor or pointer-position heuristics."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -254,6 +304,7 @@
|
|||||||
"anatomy": ["left brand", "optional left context", "center navigation/workspace", "right actions/profile"],
|
"anatomy": ["left brand", "optional left context", "center navigation/workspace", "right actions/profile"],
|
||||||
"rules": [
|
"rules": [
|
||||||
"The logo occupies the same visual position and geometry across applications.",
|
"The logo occupies the same visual position and geometry across applications.",
|
||||||
|
"Light single-color assets may opt into brandMonochrome / monochrome for theme contrast; full-color images are unchanged by default.",
|
||||||
"Product routes and profile content are application data passed into stable slots.",
|
"Product routes and profile content are application data passed into stable slots.",
|
||||||
"The canonical application header is fixed and preserves the Launcher three-axis logo / navigation / profile positions.",
|
"The canonical application header is fixed and preserves the Launcher three-axis logo / navigation / profile positions.",
|
||||||
"Consumers supply data and actions but cannot override preset geometry through local className or style props."
|
"Consumers supply data and actions but cannot override preset geometry through local className or style props."
|
||||||
@@ -394,6 +445,7 @@
|
|||||||
"summary": "Neutral administration group with title metadata, actions, content and a compact binary switch.",
|
"summary": "Neutral administration group with title metadata, actions, content and a compact binary switch.",
|
||||||
"rules": [
|
"rules": [
|
||||||
"The card owns grouping geometry but not product form schemas.",
|
"The card owns grouping geometry but not product form schemas.",
|
||||||
|
"Section and empty-state titles use the compact md token; descriptions use sm. Page/window title sizes and browser heading defaults are forbidden inside cards.",
|
||||||
"Use Switch for compact visibility/enable states; use Checker for the Engine inspector treatment."
|
"Use Switch for compact visibility/enable states; use Checker for the Engine inspector treatment."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -402,6 +454,7 @@
|
|||||||
"status": "baseline",
|
"status": "baseline",
|
||||||
"package": "@nodedc/ui-react",
|
"package": "@nodedc/ui-react",
|
||||||
"exports": ["StatusBadge"],
|
"exports": ["StatusBadge"],
|
||||||
|
"variants": ["badge", "indicator"],
|
||||||
"domContract": ["nodedc-status"],
|
"domContract": ["nodedc-status"],
|
||||||
"summary": "Semantic status pill whose tone is independent from the application accent."
|
"summary": "Semantic status pill whose tone is independent from the application accent."
|
||||||
},
|
},
|
||||||
@@ -416,6 +469,7 @@
|
|||||||
"behavior": ["controlled items", "independent 10 second default auto-dismiss per terminal item", "manual dismiss", "portal viewport", "polite live region"],
|
"behavior": ["controlled items", "independent 10 second default auto-dismiss per terminal item", "manual dismiss", "portal viewport", "polite live region"],
|
||||||
"rules": [
|
"rules": [
|
||||||
"Applications own operation state and message copy; ToastStack owns viewport geometry and dismissal timing.",
|
"Applications own operation state and message copy; ToastStack owns viewport geometry and dismissal timing.",
|
||||||
|
"Errors and status copy use the compact Toast typography; consumers do not substitute browser-default paragraphs or title-sized text.",
|
||||||
"Adding or updating another notification never restarts the lifetime of an existing item; each item owns its timer by stable id.",
|
"Adding or updating another notification never restarts the lifetime of an existing item; each item owns its timer by stable id.",
|
||||||
"Loading notifications remain until the operation updates or dismisses them.",
|
"Loading notifications remain until the operation updates or dismisses them.",
|
||||||
"Status notifications never replace a blocking confirmation modal."
|
"Status notifications never replace a blocking confirmation modal."
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"architecture": "../docs/ARCHITECTURE.md",
|
"architecture": "../docs/ARCHITECTURE.md",
|
||||||
"components": "../docs/COMPONENTS.md",
|
"components": "../docs/COMPONENTS.md",
|
||||||
"theming": "../docs/THEMING.md",
|
"theming": "../docs/THEMING.md",
|
||||||
|
"operationalTypography": "../docs/OPERATIONAL_TYPOGRAPHY.md",
|
||||||
"windows": "../docs/WINDOWS_AND_LAYERS.md",
|
"windows": "../docs/WINDOWS_AND_LAYERS.md",
|
||||||
"baseline": "../docs/SOURCE_BASELINE.md",
|
"baseline": "../docs/SOURCE_BASELINE.md",
|
||||||
"governance": "../docs/GOVERNANCE.md",
|
"governance": "../docs/GOVERNANCE.md",
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import {createElement} from "react";
|
||||||
|
import {renderToStaticMarkup} from "react-dom/server";
|
||||||
|
import {ProgressBar,ResourceRow} from "../packages/ui-react/dist/index.js";
|
||||||
|
|
||||||
|
test("progress exposes bounded measured values and unknown state without false percentages",()=>{
|
||||||
|
for(const [value, expected] of [[-1,0],[.5,50],[2,100]]) {
|
||||||
|
const html=renderToStaticMarkup(createElement(ProgressBar,{label:"Этапы",value}));
|
||||||
|
assert.match(html,new RegExp(`aria-valuenow="${expected}"`));
|
||||||
|
}
|
||||||
|
for(const value of [undefined,NaN,Infinity]) {
|
||||||
|
const html=renderToStaticMarkup(createElement(ProgressBar,{label:"Ожидание",value}));
|
||||||
|
assert.doesNotMatch(html,/aria-valuenow/);assert.match(html,/data-indeterminate="true"/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test("row keeps progress between name and actions and suppresses duplicate status",()=>{
|
||||||
|
const html=renderToStaticMarkup(createElement(ResourceRow,{title:"Камера",status:"Old status",actions:"Actions",progress:{label:"Загрузка",value:.2}}));
|
||||||
|
assert.ok(html.indexOf("Камера")<html.indexOf('role="progressbar"'));
|
||||||
|
assert.ok(html.indexOf('role="progressbar"')<html.indexOf("Actions"));
|
||||||
|
assert.doesNotMatch(html,/Old status/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user