feat(foundry): refine map application controls

This commit is contained in:
Codex
2026-08-09 14:24:18 +03:00
parent 7025fd20b5
commit beb84f6de6
18 changed files with 633 additions and 186 deletions
@@ -7,9 +7,11 @@ export interface ApplicationShellProps extends Omit<HTMLAttributes<HTMLDivElemen
stage: ReactNode;
navigation?: ReactNode;
content?: ReactNode;
endPanel?: ReactNode;
navigationOpen?: boolean;
contentOpen?: boolean;
contentExpanded?: boolean;
endPanelOpen?: boolean;
}
export function ApplicationShell({
@@ -17,9 +19,11 @@ export function ApplicationShell({
stage,
navigation,
content,
endPanel,
navigationOpen = false,
contentOpen = false,
contentExpanded = false,
endPanelOpen = false,
className,
...props
}: ApplicationShellProps) {
@@ -29,6 +33,7 @@ export function ApplicationShell({
data-navigation-open={navigationOpen ? "true" : undefined}
data-content-open={contentOpen ? "true" : undefined}
data-content-expanded={contentOpen && contentExpanded ? "true" : undefined}
data-end-panel-open={endPanelOpen ? "true" : undefined}
{...props}
>
{header}
@@ -36,6 +41,7 @@ export function ApplicationShell({
<div className="nodedc-app-shell__stage">{stage}</div>
{navigationOpen && navigation ? <div className="nodedc-app-shell__navigation">{navigation}</div> : null}
{contentOpen && content ? <div className="nodedc-app-shell__content">{content}</div> : null}
{endPanel ? <div className="nodedc-app-shell__end-panel">{endPanel}</div> : null}
</main>
</div>
);
@@ -0,0 +1,55 @@
import type { HTMLAttributes, ReactNode } from "react";
import { cn } from "./cn.js";
import { Icon } from "./Icon.js";
export interface ApplicationSidePanelProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
eyebrow?: ReactNode;
title: ReactNode;
description?: ReactNode;
closeLabel?: string;
footer?: ReactNode;
onClose: () => void;
children: ReactNode;
}
/**
* Theme-aware trailing application panel paired with AdminNavigationPanel.
*
* The shell owns the separate end column; this component owns its matching
* surface, actions and right-to-left entrance animation.
*/
export function ApplicationSidePanel({
eyebrow = "NODE.DC",
title,
description,
closeLabel = "Закрыть панель",
footer,
onClose,
children,
className,
...props
}: ApplicationSidePanelProps) {
return (
<aside
className={cn("nodedc-admin-panel nodedc-application-side-panel", className)}
data-placement="end"
{...props}
>
<header className="nodedc-admin-panel__head">
<div>
<p>{eyebrow}</p>
<h2>{title}</h2>
{description ? <span className="nodedc-application-side-panel__description">{description}</span> : null}
</div>
<div className="nodedc-admin-panel__head-actions">
<button type="button" className="nodedc-admin-panel__close" aria-label={closeLabel} onClick={onClose}>
<Icon name="close" size={16} strokeWidth={1.6} />
</button>
</div>
</header>
<div className="nodedc-application-side-panel__body">{children}</div>
{footer ? <footer className="nodedc-admin-panel__footer">{footer}</footer> : null}
</aside>
);
}
+6
View File
@@ -33,6 +33,7 @@ export interface DropdownProps {
className?: string;
surfaceClassName?: string;
surfaceRole?: "menu" | "listbox" | "dialog";
onOpenChange?: (open: boolean) => void;
}
export function Dropdown({
@@ -46,6 +47,7 @@ export function Dropdown({
className,
surfaceClassName,
surfaceRole = "menu",
onOpenChange,
}: DropdownProps) {
const instanceId = useId();
const surfaceId = `${instanceId.replaceAll(":", "")}-surface`;
@@ -55,6 +57,10 @@ export function Dropdown({
const surfaceRef = useRef<HTMLDivElement>(null);
const [surfaceStyle, setSurfaceStyle] = useState<CSSProperties>({ visibility: "hidden" });
useEffect(() => {
onOpenChange?.(isOpen);
}, [isOpen, onOpenChange]);
const close = useCallback(() => setIsOpen(false), []);
const show = useCallback(() => {
if (!disabled) setIsOpen(true);
+13 -5
View File
@@ -2,6 +2,7 @@ import { useMemo, useState, type ReactNode } from "react";
import { InspectorSelectPolicyContext } from "./InspectorContext.js";
import { Select, type SelectProps } from "./Select.js";
import { cn } from "./cn.js";
import { Icon } from "./Icon.js";
export interface InspectorSectionSpec {
id: string;
@@ -11,6 +12,7 @@ export interface InspectorSectionSpec {
content: ReactNode;
disabled?: boolean;
tone?: "default" | "accent";
icon?: ReactNode;
}
export interface InspectorProps {
@@ -19,6 +21,7 @@ export interface InspectorProps {
openSections?: string[];
activeId?: string;
singleOpen?: boolean;
variant?: "default" | "panel";
className?: string;
onOpenSectionsChange?: (ids: string[]) => void;
onActiveChange?: (id: string) => void;
@@ -30,6 +33,7 @@ export function Inspector({
openSections,
activeId,
singleOpen = false,
variant = "default",
className,
onOpenSectionsChange,
onActiveChange,
@@ -63,7 +67,7 @@ export function Inspector({
return (
<InspectorSelectPolicyContext.Provider value>
<div className={cn("nodedc-inspector", className)}>
<div className={cn("nodedc-inspector", className)} data-variant={variant === "default" ? undefined : variant}>
{groups.map((group, groupIndex) => (
<div className="nodedc-inspector__group" key={`${group.label ?? "root"}-${groupIndex}`}>
{group.sections.map((section) => {
@@ -80,10 +84,14 @@ export function Inspector({
disabled={section.disabled}
onClick={() => toggle(section.id)}
>
<span className="nodedc-inspector__section-label">{section.label}</span>
{section.description ? (
<span className="nodedc-inspector__section-description">{section.description}</span>
) : null}
<span className="nodedc-inspector__section-icon" aria-hidden="true">{section.icon ?? <Icon name="settings" />}</span>
<span className="nodedc-inspector__section-body">
<span className="nodedc-inspector__section-label">{section.label}</span>
{section.description ? (
<span className="nodedc-inspector__section-description">{section.description}</span>
) : null}
</span>
<span className="nodedc-inspector__section-chevron" aria-hidden="true"><Icon name="chevron-right" size={14} /></span>
</button>
{isOpen ? <div className="nodedc-inspector__section-content">{section.content}</div> : null}
</section>
+1
View File
@@ -1,6 +1,7 @@
export * from "./AppHeader.js";
export * from "./AdminNavigationPanel.js";
export * from "./ApplicationShell.js";
export * from "./ApplicationSidePanel.js";
export * from "./ApplicationWorkspace.js";
export * from "./Button.js";
export * from "./Checker.js";