feat(ui): add momentary keyboard control surface
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
ApplicationShell,
|
ApplicationShell,
|
||||||
ApplicationSidePanel,
|
ApplicationSidePanel,
|
||||||
Button,
|
Button,
|
||||||
|
KeyButton,
|
||||||
Checker,
|
Checker,
|
||||||
ColorField,
|
ColorField,
|
||||||
ConfirmationModal,
|
ConfirmationModal,
|
||||||
@@ -1410,6 +1411,9 @@ export function CatalogApp() {
|
|||||||
<Button variant="ghost" icon={<Icon name="external" />}>Открыть</Button>
|
<Button variant="ghost" icon={<Icon name="external" />}>Открыть</Button>
|
||||||
<Button variant="danger" icon={<Icon name="trash" />}>Удалить</Button>
|
<Button variant="danger" icon={<Icon name="trash" />}>Удалить</Button>
|
||||||
<IconButton label="Добавить"><Icon name="plus" /></IconButton>
|
<IconButton label="Добавить"><Icon name="plus" /></IconButton>
|
||||||
|
<KeyButton label="Клавиша W">W</KeyButton>
|
||||||
|
<KeyButton label="Клавиша A нажата" pressed>A</KeyButton>
|
||||||
|
<KeyButton label="Клавиша S недоступна" disabled>S</KeyButton>
|
||||||
</div>
|
</div>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Активность" note="default / compact" className="catalog-preview--compact">
|
<Preview title="Активность" note="default / compact" className="catalog-preview--compact">
|
||||||
|
|||||||
@@ -380,3 +380,7 @@ StatusBadge `variant="indicator"` отображает только одну л
|
|||||||
Общая главная: надзаголовок, заголовок, описание, быстрые действия, фон и необязательные status/footer slots. Изображения меняются по таймеру, видео — после завершения; ошибочные источники пропускаются. Пустой фон однотонный, без декоративного круга. Состояние страниц и хранение принадлежат приложению.
|
Общая главная: надзаголовок, заголовок, описание, быстрые действия, фон и необязательные status/footer slots. Изображения меняются по таймеру, видео — после завершения; ошибочные источники пропускаются. Пустой фон однотонный, без декоративного круга. Состояние страниц и хранение принадлежат приложению.
|
||||||
|
|
||||||
Для primary Button `tone="neutral"` фиксирует белый enabled и серый disabled, независимо от темы и акцента; keyboard focus остаётся видимым. MediaSourceField принимает `disabled` и блокирует все операции выбора источника, пока родитель сохраняет документ или загружает файл.
|
Для primary Button `tone="neutral"` фиксирует белый enabled и серый disabled, независимо от темы и акцента; keyboard focus остаётся видимым. MediaSourceField принимает `disabled` и блокирует все операции выбора источника, пока родитель сохраняет документ или загружает файл.
|
||||||
|
|
||||||
|
## KeyButton
|
||||||
|
|
||||||
|
Контурная клавиша удержания с контролируемым pressed-состоянием. [Контракт](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).
|
||||||
@@ -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; }
|
.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; }
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {forwardRef, type ButtonHTMLAttributes} from 'react';
|
||||||
|
import {cn} from './cn.js';
|
||||||
|
|
||||||
|
export interface KeyButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
/** 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<HTMLButtonElement, KeyButtonProps>(function KeyButton(
|
||||||
|
{pressed=false,label,children,className,type='button',disabled,...props},ref,
|
||||||
|
) {
|
||||||
|
return <button {...props} ref={ref} type={type} disabled={disabled}
|
||||||
|
aria-label={label} title={label} aria-pressed={pressed && !disabled}
|
||||||
|
className={cn('nodedc-key-button',className)}>{children}</button>;
|
||||||
|
});
|
||||||
@@ -35,3 +35,4 @@ export * from "./EnvironmentSettingsWindow.js";
|
|||||||
export * from "./EnvironmentMediaPlaylistEditor.js";
|
export * from "./EnvironmentMediaPlaylistEditor.js";
|
||||||
export * from "./EnvironmentBackgroundMedia.js";
|
export * from "./EnvironmentBackgroundMedia.js";
|
||||||
export * from "./LandingStage.js";
|
export * from "./LandingStage.js";
|
||||||
|
export {KeyButton, type KeyButtonProps} from './KeyButton.js';
|
||||||
|
|||||||
@@ -625,6 +625,29 @@
|
|||||||
"Applications supply content and actions, not duplicate stage markup.",
|
"Applications supply content and actions, not duplicate stage markup.",
|
||||||
"Background playback and scene navigation never own device acquisition."
|
"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."
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user