feat(ui): share resource rows and refine Node shell behavior
This commit is contained in:
@@ -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={
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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`: общая тема обеспечивает контраст на светлом фоне.
|
||||||
|
По умолчанию флаг выключен; цветные изображения и аватары не меняются.
|
||||||
|
|||||||
@@ -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 остаются у общего контрола. Строка не объявляет
|
||||||
|
объект доступным или подключённым самостоятельно.
|
||||||
|
|
||||||
|
Пример находится в живом каталоге, раздел «Контролы → Строки ресурсов».
|
||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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,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>;
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
Reference in New Issue
Block a user