diff --git a/apps/catalog/src/CatalogApp.tsx b/apps/catalog/src/CatalogApp.tsx
index ca05cdf..8c45003 100644
--- a/apps/catalog/src/CatalogApp.tsx
+++ b/apps/catalog/src/CatalogApp.tsx
@@ -12,6 +12,7 @@ import {
ApplicationShell,
ApplicationSidePanel,
Button,
+ KeyButton,
Checker,
ColorField,
ConfirmationModal,
@@ -1410,6 +1411,9 @@ export function CatalogApp() {
}>Открыть
}>Удалить
+ W
+ A
+ S
diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md
index 970d91e..70562b5 100644
--- a/docs/COMPONENTS.md
+++ b/docs/COMPONENTS.md
@@ -380,3 +380,7 @@ StatusBadge `variant="indicator"` отображает только одну л
Общая главная: надзаголовок, заголовок, описание, быстрые действия, фон и необязательные status/footer slots. Изображения меняются по таймеру, видео — после завершения; ошибочные источники пропускаются. Пустой фон однотонный, без декоративного круга. Состояние страниц и хранение принадлежат приложению.
Для primary Button `tone="neutral"` фиксирует белый enabled и серый disabled, независимо от темы и акцента; keyboard focus остаётся видимым. MediaSourceField принимает `disabled` и блокирует все операции выбора источника, пока родитель сохраняет документ или загружает файл.
+
+## KeyButton
+
+Контурная клавиша удержания с контролируемым pressed-состоянием. [Контракт](KEY_BUTTON.md).
diff --git a/docs/KEY_BUTTON.md b/docs/KEY_BUTTON.md
new file mode 100644
index 0000000..85097f8
--- /dev/null
+++ b/docs/KEY_BUTTON.md
@@ -0,0 +1,15 @@
+# KeyButton
+
+Owner-approved momentary keyboard/pointer visualization (Mission Core, 2026-09-25).
+Outlined square key surface; pressed is white with black text in every theme.
+This explicit outline identifies a key, not a decorative panel border.
+
+`label` is the complete accessible action; children are the short key legend.
+`pressed` is controlled and never toggled by the component. Native button keyboard
+semantics, disabled behavior and focus indicator are retained. Consumers own
+pointer capture and release, keyboard scope, blur/cancel/unmount handling and
+any domain command lease. A displayed pressed key is not an acknowledgement
+from equipment. Disabled always renders released. No global listeners exist.
+
+Catalog includes idle, pressed, disabled and focusable surfaces. Use only for
+momentary key input, not ordinary actions (Button) or mode choice (SegmentedControl).
diff --git a/packages/ui-core/styles.css b/packages/ui-core/styles.css
index dda0312..7bb8b2a 100644
--- a/packages/ui-core/styles.css
+++ b/packages/ui-core/styles.css
@@ -4302,3 +4302,13 @@ textarea.nodedc-field__control {
}
}
.nodedc-landing-stage__eyebrow { color: var(--nodedc-text-muted); font-size: var(--nodedc-font-size-xs); font-weight: var(--nodedc-font-weight-strong); letter-spacing: .12em; }
+
+/* Explicit key outline: approved momentary-input affordance, not a card rim. */
+.nodedc-key-button { display:inline-grid; place-items:center; width:46px; height:46px;
+ border:1px solid var(--nodedc-text-secondary); border-radius:var(--nodedc-radius-sm, 8px);
+ background:transparent; color:var(--nodedc-text-primary); font:inherit; font-weight:600;
+ cursor:pointer; user-select:none; touch-action:none; }
+.nodedc-key-button:hover:not(:disabled) { background:var(--nodedc-glass-control-hover); }
+.nodedc-key-button[aria-pressed="true"]:not(:disabled) { background:#fff; color:#000; border-color:#fff; }
+.nodedc-key-button:disabled { opacity:.4; cursor:not-allowed; }
+.nodedc-key-button:focus-visible { outline:2px solid rgb(var(--nodedc-accent-rgb)); outline-offset:3px; }
diff --git a/packages/ui-react/src/KeyButton.tsx b/packages/ui-react/src/KeyButton.tsx
new file mode 100644
index 0000000..c62ef03
--- /dev/null
+++ b/packages/ui-react/src/KeyButton.tsx
@@ -0,0 +1,17 @@
+import {forwardRef, type ButtonHTMLAttributes} from 'react';
+import {cn} from './cn.js';
+
+export interface KeyButtonProps extends ButtonHTMLAttributes {
+ /** Controlled physical/pointer input indication; this is never a toggle. */
+ pressed?: boolean;
+ label: string;
+}
+
+/** A momentary key surface. The consumer owns input, focus scope and release policy. */
+export const KeyButton = forwardRef(function KeyButton(
+ {pressed=false,label,children,className,type='button',disabled,...props},ref,
+) {
+ return ;
+});
diff --git a/packages/ui-react/src/index.ts b/packages/ui-react/src/index.ts
index bf3df57..9477158 100644
--- a/packages/ui-react/src/index.ts
+++ b/packages/ui-react/src/index.ts
@@ -35,3 +35,4 @@ export * from "./EnvironmentSettingsWindow.js";
export * from "./EnvironmentMediaPlaylistEditor.js";
export * from "./EnvironmentBackgroundMedia.js";
export * from "./LandingStage.js";
+export {KeyButton, type KeyButtonProps} from './KeyButton.js';
diff --git a/registry/components.json b/registry/components.json
index c120c08..dd8c648 100644
--- a/registry/components.json
+++ b/registry/components.json
@@ -625,6 +625,29 @@
"Applications supply content and actions, not duplicate stage markup.",
"Background playback and scene navigation never own device acquisition."
]
+ },
+ {
+ "id": "key-button",
+ "status": "baseline",
+ "package": "@nodedc/ui-react",
+ "exports": [
+ "KeyButton",
+ "KeyButtonProps"
+ ],
+ "domContract": [
+ "nodedc-key-button"
+ ],
+ "summary": "Controlled outlined momentary input key; white pressed surface with black legend.",
+ "states": [
+ "idle",
+ "pressed",
+ "disabled",
+ "focus-visible"
+ ],
+ "rules": [
+ "Consumer owns command lifecycle and blur/cancel release.",
+ "No internal toggle or global keyboard handler."
+ ]
}
]
}