feat(ui): share resource rows and refine Node shell behavior

This commit is contained in:
Codex
2026-09-05 17:25:27 +03:00
parent d51d8bb7f6
commit 8a79dfe84d
9 changed files with 129 additions and 5 deletions
+9
View File
@@ -29,6 +29,8 @@ import {
InspectorSelectField, InspectorSelectField,
MediaSourceField, MediaSourceField,
RangeControl, RangeControl,
ResourceRow,
ResourceList,
SegmentedControl, SegmentedControl,
Select, Select,
ShareAccessModal, ShareAccessModal,
@@ -1408,6 +1410,12 @@ 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="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={<ActivityIndicator size="compact" />} title="Получение сведений" metadata="Действие выполняется" aria-busy="true" actions={<IconButton label="Просмотр недоступен" disabled><Icon name="eye" /></IconButton>} /></li>
</ResourceList>
</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 +2000,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={
+4
View File
@@ -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.
+9
View File
@@ -312,3 +312,12 @@ 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`: общая тема обеспечивает контраст на светлом фоне.
По умолчанию флаг выключен; цветные изображения и аватары не меняются.
+23
View File
@@ -0,0 +1,23 @@
# 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 остаются у общего контрола. Строка не объявляет
объект доступным или подключённым самостоятельно.
Пример находится в живом каталоге, раздел «Контролы → Строки ресурсов».
+20 -1
View File
@@ -3680,7 +3680,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 +3777,22 @@ 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); }
@media (max-width: 540px) {
.nodedc-resource-row { flex-wrap: wrap; }
.nodedc-resource-row__copy { flex-basis: calc(100% - 3rem); }
.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); }
+8 -4
View File
@@ -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>
); );
+30
View File
@@ -0,0 +1,30 @@
import type { HTMLAttributes, ReactNode } from "react";
import { cn } from "./cn.js";
export interface ResourceRowProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
title: ReactNode;
icon?: ReactNode;
description?: ReactNode;
metadata?: ReactNode;
status?: ReactNode;
actions?: ReactNode;
}
/** 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, actions, className, ...props }: ResourceRowProps) {
return <div className={cn("nodedc-resource-row", className)} {...props}>
{icon ? <span className="nodedc-resource-row__icon" aria-hidden="true">{icon}</span> : null}
<div className="nodedc-resource-row__copy">
<strong>{title}</strong>
{description ? <span>{description}</span> : null}
{metadata ? <small>{metadata}</small> : null}
</div>
{status ? <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>;
}
+1
View File
@@ -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";
+25
View File
@@ -1,6 +1,30 @@
{ {
"schemaVersion": "1.0.0", "schemaVersion": "1.0.0",
"components": [ "components": [
{
"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",
"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",
@@ -254,6 +278,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."