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 {
|
||||
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={
|
||||
|
||||
@@ -44,3 +44,7 @@ Modal остаётся portal-слоем. Его тело прокручивае
|
||||
- предметное содержимое content window.
|
||||
|
||||
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 автоматически.
|
||||
|
||||
## 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
@@ -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, потому что изменение распространяется на все будущие приложения.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
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)); }
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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> {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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