Compare commits

8 Commits
16 changed files with 322 additions and 14 deletions
+19
View File
@@ -3,6 +3,8 @@ import { applyGlassMaterial, applyNodedcTheme, defaultGlassMaterial, type GlassM
import { createTemplateFeatures, getPageTemplate, pageTemplates, type PageTemplateDefinition } from "@nodedc/page-patterns";
import {
ActivityIndicator,
StatusBadge,
ProgressBar,
AdminNavigationPanel,
AppHeader,
ApplicationPanel,
@@ -29,6 +31,8 @@ import {
InspectorSelectField,
MediaSourceField,
RangeControl,
ResourceRow,
ResourceList,
SegmentedControl,
Select,
ShareAccessModal,
@@ -1408,6 +1412,20 @@ export function CatalogApp() {
</div>
<p className="catalog-preview__explanation">При reduced motion кольцо остаётся видимым без вращения; процесс и его завершение принадлежат приложению.</p>
</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">
<div className="catalog-window-actions-demo">
<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" />}
header={
<AppHeader
brandMonochrome
brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />}
brandHref="/"
center={
+4
View File
@@ -44,3 +44,7 @@ Modal остаётся portal-слоем. Его тело прокручивае
- предметное содержимое content window.
Shell владеет геометрией, слоями, переходами и breakpoint-поведением. См. готовый путь подключения в `docs/CONSUMPTION.md`.
На узком экране скрывается именно действие `data-action="expand"`.
Порядок utility actions не является признаком типа действия: плюс и обновление
остаются доступны, даже когда стоят первыми в шапке. Исправлено при QA Node 0.3.
+24
View File
@@ -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 автоматически.
## WorkspaceWindow
@@ -158,6 +160,8 @@ ConfirmationModal оборачивает Window и защищает async-confir
`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.
Разделитель является структурной границей между двумя рабочими представлениями и не создаёт декоративную рамку вокруг контента. При `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 продолжает владеть определениями полей, типами нод и сохранением значений. В живом 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
View File
@@ -29,6 +29,12 @@
Допустимы 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
Перед удалением export:
@@ -53,4 +59,3 @@
## Владение
У дизайн-системы должен быть явный code owner. Product team может предлагать компоненты, но общий API и theme contract проходят отдельное review, потому что изменение распространяется на все будущие приложения.
+22
View File
@@ -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 h1h6/p/strong и локальные крупные размеры запрещены.
- Размер не умножается из-за вложенности карточек, ошибки или отсутствия данных.
SettingsCard и nodedc-empty-state закрепляют эти размеры в ui-core. Текст
«Устройства не обнаружены» не является заголовком нового раздела. В живом
каталоге рядом со строками ресурсов есть пример пустого списка.
В завершённом этапе настройки окружения status-слот ResourceRow содержит
только Icon check с доступным названием. Галочка не оборачивается в StatusBadge,
Checker, круг, pill или кнопку. Это знак результата, а не интерактивный контрол.
Лампочка состояния устройства отдельно использует StatusBadge indicator.
+15
View File
@@ -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, на узком экране переносится ниже текста. Готовность не вычисляется
компонентом. Каталог содержит завершённый, определённый и неизвестный прогресс.
+34
View File
@@ -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.
+48 -4
View File
@@ -585,13 +585,13 @@ textarea.nodedc-field__control {
.nodedc-settings-card__titles h2 {
color: var(--nodedc-text-primary);
font-size: 1rem;
font-size: var(--nodedc-font-size-md);
line-height: 1.25;
}
.nodedc-settings-card__titles p {
color: var(--nodedc-text-muted);
font-size: 0.75rem;
font-size: var(--nodedc-font-size-sm);
line-height: 1.4;
}
@@ -3569,6 +3569,7 @@ textarea.nodedc-field__control {
}
.nodedc-empty-state {
font-size: var(--nodedc-font-size-sm);
display: grid;
min-height: 12rem;
place-items: center;
@@ -3580,7 +3581,7 @@ textarea.nodedc-field__control {
.nodedc-empty-state strong {
color: var(--nodedc-text-primary);
font-size: var(--nodedc-font-size-lg);
font-size: var(--nodedc-font-size-md);
}
@media (max-width: 1100px) {
@@ -3680,7 +3681,7 @@ textarea.nodedc-field__control {
display: none;
}
.nodedc-application-panel__action:first-child {
.nodedc-application-panel__action[data-action="expand"] {
display: none;
}
@@ -3777,3 +3778,46 @@ textarea.nodedc-field__control {
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)); }
+8 -4
View File
@@ -4,6 +4,8 @@ export interface AppHeaderProps {
brand: ReactNode;
brandHref?: string;
brandLabel?: string;
/** Opt in only for a light, single-color mark, never a full-color image. */
brandMonochrome?: boolean;
left?: ReactNode;
center?: ReactNode;
right?: ReactNode;
@@ -13,14 +15,15 @@ export function AppHeader({
brand,
brandHref,
brandLabel = "NODE.DC",
brandMonochrome = false,
left,
center,
right,
}: AppHeaderProps) {
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 (
@@ -100,11 +103,12 @@ export interface HeaderWorkspaceProps {
label: string;
imageUrl?: string;
kind?: "mark" | "avatar";
monochrome?: boolean;
}
export function HeaderWorkspace({ label, imageUrl, kind = "mark" }: HeaderWorkspaceProps) {
export function HeaderWorkspace({ label, imageUrl, kind = "mark", monochrome = false }: HeaderWorkspaceProps) {
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()}
</span>
);
+20
View File
@@ -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>;
}
+36
View File
@@ -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>;
}
+4 -3
View File
@@ -5,12 +5,13 @@ export type StatusTone = "neutral" | "success" | "warning" | "danger" | "accent"
export interface StatusBadgeProps extends HTMLAttributes<HTMLSpanElement> {
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 (
<span className={cn("nodedc-status", className)} data-tone={tone === "neutral" ? undefined : tone} {...props}>
{children}
<span className={cn("nodedc-status", className)} data-tone={tone === "neutral" ? undefined : tone} data-variant={variant === "indicator" ? variant : undefined} {...props}>
{variant === "indicator" ? null : children}
</span>
);
}
+3
View File
@@ -16,6 +16,7 @@ export * from "./Inspector.js";
export * from "./Icon.js";
export * from "./MediaSourceField.js";
export * from "./RangeControl.js";
export * from "./ResourceRow.js";
export * from "./SegmentedControl.js";
export * from "./Select.js";
export * from "./StatusBadge.js";
@@ -27,3 +28,5 @@ export * from "./Toast.js";
export * from "./UserProfileMenu.js";
export * from "./Window.js";
export * from "./WorkspaceWindow.js";
export { ProgressBar, type ProgressBarProps } from "./ProgressBar.js";
+56 -2
View File
@@ -1,6 +1,53 @@
{
"schemaVersion": "1.0.0",
"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",
"status": "baseline",
@@ -160,7 +207,9 @@
"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.",
"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": [
"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.",
"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"],
"rules": [
"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.",
"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."
@@ -394,6 +445,7 @@
"summary": "Neutral administration group with title metadata, actions, content and a compact binary switch.",
"rules": [
"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."
]
},
@@ -402,6 +454,7 @@
"status": "baseline",
"package": "@nodedc/ui-react",
"exports": ["StatusBadge"],
"variants": ["badge", "indicator"],
"domContract": ["nodedc-status"],
"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"],
"rules": [
"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.",
"Loading notifications remain until the operation updates or dismisses them.",
"Status notifications never replace a blocking confirmation modal."
+1
View File
@@ -44,6 +44,7 @@
"architecture": "../docs/ARCHITECTURE.md",
"components": "../docs/COMPONENTS.md",
"theming": "../docs/THEMING.md",
"operationalTypography": "../docs/OPERATIONAL_TYPOGRAPHY.md",
"windows": "../docs/WINDOWS_AND_LAYERS.md",
"baseline": "../docs/SOURCE_BASELINE.md",
"governance": "../docs/GOVERNANCE.md",
+22
View File
@@ -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/);
});