diff --git a/apps/catalog/src/CatalogApp.tsx b/apps/catalog/src/CatalogApp.tsx
index d1a9924..c9b0bb1 100644
--- a/apps/catalog/src/CatalogApp.tsx
+++ b/apps/catalog/src/CatalogApp.tsx
@@ -529,12 +529,10 @@ export function CatalogApp() {
Настройки окружения ENGINE / новый канон
-
- {environmentLayoutSaved ? Layout сохранён локально. Сервер и MCP не используются.
: null}
-
Environment controls — единый референс Engine: palette, range, checker, split-select и раскрывающиеся sections. Верхняя секция меняет цвет плашек и полей приложения; сохраняется весь текущий layout одной кнопкой сверху.
+ Environment controls — единый референс Engine: palette, range, checker, split-select и раскрывающиеся sections. Верхняя секция меняет цвет плашек и полей приложения; весь layout сохраняется общей кнопкой в шапке окна.
@@ -705,7 +703,7 @@ export function CatalogApp() {
}
right={
-
+
Профиль
@@ -755,6 +753,11 @@ export function CatalogApp() {
description={activeDefinition.description}
expanded={panelExpanded}
onExpandedChange={workspace.setContentExpanded}
+ utilityActions={[{
+ label: environmentLayoutSaved ? "Layout сохранён локально" : "Сохранить layout",
+ icon: "save",
+ onClick: saveEnvironmentLayout,
+ }]}
onClose={workspace.closeView}
>
{renderSectionContent()}
diff --git a/docs/APPLICATION_TEMPLATE.md b/docs/APPLICATION_TEMPLATE.md
index 60d44d2..df90d3a 100644
--- a/docs/APPLICATION_TEMPLATE.md
+++ b/docs/APPLICATION_TEMPLATE.md
@@ -8,10 +8,12 @@
2. `stage` — главное медиа- или предметное окно приложения.
3. `navigation` — левая панель шириной `clamp(332px, 19.5vw, 352px)`.
4. `content` — отдельное рабочее окно справа от навигации.
-5. `ApplicationPanel` — заголовок, действия expand/close и независимо прокручиваемое тело контента.
+5. `ApplicationPanel` — заголовок, постоянные utility actions, действия expand/close и независимо прокручиваемое тело контента.
Позиции шапки не пересчитываются под конкретное приложение. Меняются подписи, доступные navigation actions, профиль и содержимое stage. `HeaderNavigation`, `HeaderWorkspace`, `HeaderProfile`, `HeaderProfileButton` и `HeaderAvatar` должны использоваться как готовые части пресета; consumer не назначает им собственные размеры и offsets.
+Действие, относящееся ко всему рабочему окну, передаётся через `ApplicationPanel.utilityActions`. Например, сохранение настроенного layout всегда находится в шапке перед expand/close и остаётся доступным при переходе между разделами; отдельные карточки не рисуют собственные копии этой кнопки.
+
## Состояния desktop
| Состояние | Навигация | Content window | Stage |
diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md
index e6d959a..74c00a6 100644
--- a/docs/COMPONENTS.md
+++ b/docs/COMPONENTS.md
@@ -122,6 +122,8 @@ Pill navigation для верхней панели и компактного п
`useApplicationWorkspace` является каноническим контроллером этих состояний для React. Приложение не должно заново связывать набор локальных boolean-state для navigation/content/expand. DOM-проекты используют идентичный `createApplicationWorkspaceController`.
+Постоянные действия всего рабочего окна передаются через `ApplicationPanel.utilityActions` и всегда находятся в правой части шапки перед expand/close. Сохранение общего layout или настроек приложения — одно такое действие для всего окна; оно не дублируется внутри отдельных карточек и секций.
+
Точные состояния и размеры зафиксированы в `docs/APPLICATION_TEMPLATE.md`.
## MediaSourceField
@@ -142,7 +144,7 @@ Pill navigation для верхней панели и компактного п
## Icon
-`Icon` предоставляет только подтверждённый общий subset иконок. Каноническое имя описывает смысл (`close`, `expand`, `refresh`), а не конкретный путь SVG. Default glyph — `16 px`, stroke — `1.6`; внутри кнопок glyph оптически уменьшается до `78%`, но размеры surface/hit target остаются прежними.
+`Icon` предоставляет только подтверждённый общий subset иконок. Каноническое имя описывает смысл (`close`, `expand`, `refresh`), а не конкретный путь SVG. Default glyph — `16 px`, stroke — `1.6`; размер не уменьшается дополнительным CSS-scale. В profile-group шапки допустим подтверждённый Launcher-размер `20 px`. Размеры surface/hit target при этом остаются независимыми и не уменьшаются.
Поверхность, круглая форма, hit target, active и disabled состояния принадлежат `IconButton`, `Button` или navigation item. Полный список находится в `registry/icons.json` и `docs/ICONS.md`.
diff --git a/packages/tokens/themes.css b/packages/tokens/themes.css
index 9a474e1..3a12c50 100644
--- a/packages/tokens/themes.css
+++ b/packages/tokens/themes.css
@@ -44,12 +44,14 @@
--nodedc-app-panel-shadow-strong: 0 34px 110px rgba(0, 0, 0, 0.52);
--nodedc-panel-action-border: rgba(255, 255, 255, 0.22);
--nodedc-panel-action-border-hover: rgba(255, 255, 255, 0.3);
+ --nodedc-panel-action-bg: transparent;
+ --nodedc-panel-action-shadow: none;
--nodedc-panel-item-bg: rgba(255, 255, 255, 0.04);
--nodedc-panel-item-hover-bg: rgba(255, 255, 255, 0.085);
--nodedc-panel-item-active-bg: rgba(255, 255, 255, 0.115);
--nodedc-panel-icon-bg: rgba(255, 255, 255, 0.045);
--nodedc-panel-active-icon-bg: rgba(247, 248, 244, 0.96);
- --nodedc-panel-active-icon-color: rgb(var(--nodedc-on-accent-rgb));
+ --nodedc-panel-active-icon-color: rgba(8, 8, 10, 0.96);
--nodedc-scrollbar-thumb: rgba(255, 255, 255, 0.15);
--nodedc-primary-action-bg: rgb(var(--nodedc-accent-rgb));
--nodedc-primary-action-hover: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 84%, white);
@@ -105,6 +107,8 @@
--nodedc-app-panel-shadow-strong: 0 18px 44px rgba(0, 0, 0, 0.14);
--nodedc-panel-action-border: rgba(24, 32, 29, 0.12);
--nodedc-panel-action-border-hover: rgba(24, 32, 29, 0.22);
+ --nodedc-panel-action-bg: #ffffff;
+ --nodedc-panel-action-shadow: 0 10px 24px rgba(24, 32, 29, 0.08);
--nodedc-panel-item-bg: #f4f4f4;
--nodedc-panel-item-hover-bg: #eeeeee;
--nodedc-panel-item-active-bg: #e8e8e8;
diff --git a/packages/tokens/tokens.css b/packages/tokens/tokens.css
index 29e5662..1f35e9e 100644
--- a/packages/tokens/tokens.css
+++ b/packages/tokens/tokens.css
@@ -25,7 +25,7 @@
--nodedc-inspector-button-radius: 1rem;
--nodedc-select-toggle-width: 2.875rem;
--nodedc-select-gap: 0.5rem;
- --nodedc-icon-glyph-scale: 0.78;
+ --nodedc-icon-glyph-scale: 1;
--nodedc-space-1: 0.25rem;
--nodedc-space-2: 0.5rem;
diff --git a/packages/ui-core/styles.css b/packages/ui-core/styles.css
index c19654a..c1327ba 100644
--- a/packages/ui-core/styles.css
+++ b/packages/ui-core/styles.css
@@ -1041,6 +1041,11 @@ textarea.nodedc-field__control {
box-shadow: none;
}
+.nodedc-header__profile .nodedc-icon-button > svg {
+ width: 1.25rem;
+ height: 1.25rem;
+}
+
.nodedc-header__profile-button {
display: inline-flex;
min-height: var(--nodedc-header-control-height);
@@ -1307,8 +1312,9 @@ textarea.nodedc-field__control {
border: 1px solid var(--nodedc-panel-action-border);
border-radius: var(--nodedc-radius-circle);
outline: 0;
- background: transparent;
+ background: var(--nodedc-panel-action-bg);
color: var(--nodedc-text-secondary);
+ box-shadow: var(--nodedc-panel-action-shadow);
cursor: pointer;
}
@@ -1319,7 +1325,7 @@ textarea.nodedc-field__control {
color: var(--nodedc-text-primary);
}
-.nodedc-application-panel[data-expanded="true"] .nodedc-application-panel__action:first-child {
+.nodedc-application-panel__action[data-active="true"] {
border-color: transparent;
background: var(--nodedc-glass-control-active);
color: var(--nodedc-glass-control-active-text);
@@ -1598,6 +1604,10 @@ textarea.nodedc-field__control {
color: var(--nodedc-panel-active-icon-color);
}
+.nodedc-admin-panel__nav-item[data-active="true"] .nodedc-admin-panel__nav-icon > svg {
+ color: var(--nodedc-panel-active-icon-color);
+}
+
.nodedc-admin-panel__footer {
width: calc(100% + 2.2rem);
margin-inline: -1.1rem;
diff --git a/packages/ui-react/src/ApplicationShell.tsx b/packages/ui-react/src/ApplicationShell.tsx
index 1530671..7b74171 100644
--- a/packages/ui-react/src/ApplicationShell.tsx
+++ b/packages/ui-react/src/ApplicationShell.tsx
@@ -1,6 +1,6 @@
import type { HTMLAttributes, ReactNode } from "react";
import { cn } from "./cn.js";
-import { Icon } from "./Icon.js";
+import { Icon, type IconName } from "./Icon.js";
export interface ApplicationShellProps extends Omit, "content"> {
header: ReactNode;
@@ -49,10 +49,19 @@ export interface ApplicationPanelProps extends Omit,
expandLabel?: string;
closeLabel?: string;
onExpandedChange?: (expanded: boolean) => void;
+ /** Persistent utility actions rendered before expand/close in the panel header. */
+ utilityActions?: ApplicationPanelUtilityAction[];
onClose: () => void;
children: ReactNode;
}
+export interface ApplicationPanelUtilityAction {
+ label: string;
+ icon: IconName;
+ onClick: () => void;
+ disabled?: boolean;
+}
+
export function ApplicationPanel({
eyebrow,
title,
@@ -61,6 +70,7 @@ export function ApplicationPanel({
expandLabel = "Развернуть окно",
closeLabel = "Закрыть окно",
onExpandedChange,
+ utilityActions = [],
onClose,
children,
className,
@@ -75,18 +85,33 @@ export function ApplicationPanel({
{description ? {description}
: null}
+ {utilityActions.map((action) => (
+
+
+
+ ))}
{onExpandedChange ? (
onExpandedChange(!expanded)}
>
-
+
) : null}
-
-
+
+
diff --git a/registry/components.json b/registry/components.json
index 38cc7fc..47baaba 100644
--- a/registry/components.json
+++ b/registry/components.json
@@ -196,13 +196,14 @@
"domExports": ["createApplicationWorkspaceController"],
"domContract": ["nodedc-app-shell", "nodedc-application-panel"],
"summary": "Launcher-derived application template with fixed header, media stage, left navigation and a separate expandable content window.",
- "anatomy": ["fixed AppHeader", "stage", "left navigation", "right content window", "expand and close actions"],
- "behavior": ["one shared navigation/content state machine", "navigation shifts stage", "content opens beside navigation", "expanded content replaces stage", "mobile panels use the available viewport sequentially", "independent content scrolling"],
+ "anatomy": ["fixed AppHeader", "stage", "left navigation", "right content window", "persistent utility actions", "expand and close actions"],
+ "behavior": ["one shared navigation/content state machine", "navigation shifts stage", "content opens beside navigation", "expanded content replaces stage", "global utility actions remain in the panel header across sections", "mobile panels use the available viewport sequentially", "independent content scrolling"],
"rules": [
"The bottom Launcher service rail is product-specific and is not part of the general application template.",
"Desktop page gap is 20 px and navigation width follows the Launcher 332–352 px range.",
"Applications supply routes, profile data, stage media and panel content without changing shell geometry.",
- "Light panels use the SEO white-glass surface tokens rather than a dark shell with inverted text."
+ "Light panels use the SEO white surface and shadow tokens rather than a dark shell with inverted text.",
+ "A layout-level save action belongs to ApplicationPanel.utilityActions and is never duplicated inside individual content cards."
]
},
{