feat(ui): support invisible resizable split separators

This commit is contained in:
Codex
2026-09-25 19:13:39 +03:00
parent a5a73c8753
commit 9aa6f949b0
6 changed files with 28 additions and 1 deletions
+3
View File
@@ -376,6 +376,7 @@ export function CatalogApp() {
const [workspaceWindowDemoOpen, setWorkspaceWindowDemoOpen] = useState(true);
const [workspaceWindowDemoMaximized, setWorkspaceWindowDemoMaximized] = useState(false);
const [workspaceWindowDemoRect, setWorkspaceWindowDemoRect] = useState<WorkspaceWindowRect>({ x: 24, y: 24, width: 340, height: 230 });
const [splitPaneInvisible, setSplitPaneInvisible] = useState(false);
const [splitPaneDemoSize, setSplitPaneDemoSize] = useState(50);
const [sidePanelDemoOpen, setSidePanelDemoOpen] = useState(true);
const [createModalOpen, setCreateModalOpen] = useState(false);
@@ -1510,9 +1511,11 @@ export function CatalogApp() {
<p className="catalog-preview__explanation">Rectangle, maximize, close и stacking остаются состоянием приложения; дизайн-система владеет одинаковой bounded-геометрией и доступным управлением.</p>
</Preview>
<Preview title="Split pane" note="pointer / keyboard / controlled" className="catalog-preview--wide catalog-split-pane-preview">
<Switch label="Скрыть линию разделителя" checked={splitPaneInvisible} onChange={setSplitPaneInvisible} />
<div className="catalog-split-pane-demo">
<SplitPane
primarySize={splitPaneDemoSize}
separatorAppearance={splitPaneInvisible ? "invisible" : "line"}
onPrimarySizeChange={setSplitPaneDemoSize}
minPrimarySize={25}
minSecondarySize={25}
+2
View File
@@ -184,6 +184,8 @@ ConfirmationModal оборачивает Window и защищает async-confir
Разделитель является структурной границей между двумя рабочими представлениями и не создаёт декоративную рамку вокруг контента. При `resizable={false}` панели сохраняют ту же стабильную DOM-композицию, а separator полностью отсутствует в визуальном слое и accessibility tree; это позволяет приложению сворачивать одну из панелей без remount тяжёлого renderer.
При `separatorAppearance="invisible"` линия скрыта в покое, при наведении и перетаскивании. Зона захвата, курсор, клавиатурное изменение размера и индикатор клавиатурного фокуса сохраняются.
## ShareAccessModal
Workflow-sharing modal из нового Engine: заголовок ресурса, compact avatar stack, список участников, редактирование ролей, удаление доступа, email и роль нового участника. Компонент сохраняет Engine-геометрию `600 px` и controls `46 px`, но не импортирует ACL API. Consumer передаёт members, permissions и callbacks.
+6
View File
@@ -3111,6 +3111,12 @@ textarea.nodedc-field__control {
box-shadow: 0 0 0 2px rgb(var(--nodedc-accent-rgb) / 0.16);
}
/* Keep the full hit area and keyboard focus cue without a visible divider. */
.nodedc-split-pane[data-separator-appearance="invisible"] > .nodedc-split-pane__separator:not(:focus-visible)::before {
background: transparent;
box-shadow: none;
}
.nodedc-split-pane[data-dragging="true"] {
user-select: none;
}
+3
View File
@@ -23,6 +23,7 @@ export interface SplitPaneProps extends Omit<HTMLAttributes<HTMLDivElement>, "ch
step?: number;
resizable?: boolean;
separatorLabel: string;
separatorAppearance?: "line" | "invisible";
}
const clamp = (value: number, minimum: number, maximum: number) => (
@@ -48,6 +49,7 @@ export function SplitPane({
step = 2,
resizable = true,
separatorLabel,
separatorAppearance = "line",
className,
style,
...props
@@ -95,6 +97,7 @@ export function SplitPane({
ref={rootRef}
className={cn("nodedc-split-pane", className)}
data-orientation={orientation}
data-separator-appearance={separatorAppearance}
data-dragging={dragging ? "true" : undefined}
style={splitStyle}
{...props}
+2 -1
View File
@@ -288,7 +288,8 @@
"summary": "Controlled two-panel layout with a pointer- and keyboard-resizable accessible separator.",
"anatomy": ["primary panel", "structural separator", "secondary panel"],
"variants": ["vertical", "horizontal"],
"behavior": ["controlled percentage", "optional resizable separator", "pointer capture drag", "Arrow/Home/End keyboard sizing", "ARIA separator range", "ratio preservation across host resize"],
"behavior": ["controlled percentage", "optional resizable separator",
"line or invisible separator appearance with retained keyboard focus", "pointer capture drag", "Arrow/Home/End keyboard sizing", "ARIA separator range", "ratio preservation across host resize"],
"rules": [
"The application owns the controlled primary percentage and panel content; the component owns separator interaction and accessibility.",
"Minimum primary and secondary percentages clamp both pointer and keyboard changes.",
+12
View File
@@ -61,3 +61,15 @@ test("SplitPane is registered, documented, cataloged and keyboard-addressable",
assert.match(docs, /## SplitPane/);
assert.match(catalog, /<SplitPane/);
});
test("invisible divider retains an accessible resize target", () => {
const markup = renderToStaticMarkup(createElement(SplitPane, {
primary: "Camera", secondary: "Map", primarySize: 40,
onPrimarySizeChange: () => {}, separatorLabel: "Resize", separatorAppearance: "invisible",
}));
assert.match(markup, /data-separator-appearance="invisible"/);
assert.match(markup, /role="separator"/);
assert.match(markup, /tabindex="0"/);
assert.match(markup, /aria-valuenow="40"/);
});