feat(foundry): refine map application controls
This commit is contained in:
@@ -37,6 +37,8 @@
|
||||
--nodedc-map-glass-active: rgba(255, 255, 255, 0.92);
|
||||
--nodedc-map-glass-text: rgba(255, 255, 255, 0.96);
|
||||
--nodedc-map-glass-text-muted: rgba(255, 255, 255, 0.72);
|
||||
--nodedc-map-field-text: rgba(13, 14, 17, 0.96);
|
||||
--nodedc-map-field-text-muted: rgba(13, 14, 17, 0.54);
|
||||
--nodedc-field-bg: rgb(var(--nodedc-field-material-rgb) / var(--nodedc-field-material-opacity));
|
||||
--nodedc-overlay-bg: rgba(0, 0, 0, 0.46);
|
||||
--nodedc-glass-rim: transparent;
|
||||
@@ -108,6 +110,8 @@
|
||||
--nodedc-map-glass-active: rgba(255, 255, 255, 0.92);
|
||||
--nodedc-map-glass-text: rgba(255, 255, 255, 0.96);
|
||||
--nodedc-map-glass-text-muted: rgba(255, 255, 255, 0.72);
|
||||
--nodedc-map-field-text: rgba(13, 14, 17, 0.96);
|
||||
--nodedc-map-field-text-muted: rgba(13, 14, 17, 0.54);
|
||||
--nodedc-field-bg: rgb(var(--nodedc-field-material-rgb) / var(--nodedc-field-material-opacity));
|
||||
--nodedc-overlay-bg: rgba(24, 32, 29, 0.22);
|
||||
--nodedc-glass-rim: transparent;
|
||||
|
||||
+163
-3
@@ -1376,7 +1376,8 @@ textarea.nodedc-field__control {
|
||||
}
|
||||
|
||||
.nodedc-app-shell__navigation,
|
||||
.nodedc-app-shell__content {
|
||||
.nodedc-app-shell__content,
|
||||
.nodedc-app-shell__end-panel {
|
||||
position: absolute;
|
||||
z-index: var(--nodedc-layer-panel);
|
||||
top: 0;
|
||||
@@ -1395,6 +1396,27 @@ textarea.nodedc-field__control {
|
||||
transition: width 500ms cubic-bezier(0.22, 1, 0.36, 1), right 500ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.nodedc-app-shell__end-panel {
|
||||
right: var(--nodedc-app-page-pad);
|
||||
width: var(--nodedc-app-nav-width);
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nodedc-app-shell__end-panel > * {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nodedc-app-shell[data-end-panel-open="true"] .nodedc-app-shell__end-panel {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.nodedc-app-shell[data-end-panel-open="true"] .nodedc-app-shell__stage {
|
||||
padding-right: calc(var(--nodedc-app-page-pad) + var(--nodedc-app-nav-width) + var(--nodedc-app-panel-gap));
|
||||
}
|
||||
|
||||
.nodedc-app-shell[data-navigation-open="true"] .nodedc-app-shell__stage {
|
||||
padding-left: calc(var(--nodedc-app-page-pad) + var(--nodedc-app-nav-width) + var(--nodedc-app-panel-gap));
|
||||
}
|
||||
@@ -1411,6 +1433,10 @@ textarea.nodedc-field__control {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nodedc-app-shell[data-content-open="true"][data-content-expanded="true"][data-end-panel-open="true"] .nodedc-app-shell__content {
|
||||
right: calc(var(--nodedc-app-page-pad) + var(--nodedc-app-nav-width) + var(--nodedc-app-panel-gap));
|
||||
}
|
||||
|
||||
.nodedc-app-shell[data-content-open="true"][data-content-expanded="true"] .nodedc-app-shell__stage {
|
||||
pointer-events: none;
|
||||
transform: translateX(calc(100vw + var(--nodedc-app-page-pad)));
|
||||
@@ -1559,6 +1585,34 @@ textarea.nodedc-field__control {
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.nodedc-application-side-panel {
|
||||
animation-name: nodedc-application-side-panel-in;
|
||||
}
|
||||
|
||||
@keyframes nodedc-application-side-panel-in {
|
||||
from { opacity: 0; transform: translateX(1.6rem); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
.nodedc-application-side-panel__description {
|
||||
display: block;
|
||||
margin-top: 0.3rem;
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.nodedc-application-side-panel__body {
|
||||
width: calc(100% + 2.2rem);
|
||||
min-height: 0;
|
||||
margin-inline: -1.1rem;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-inline: 1.1rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--nodedc-scrollbar-thumb) transparent;
|
||||
}
|
||||
|
||||
@keyframes nodedc-admin-panel-in {
|
||||
from { opacity: 0; transform: translateX(-1.6rem); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
@@ -3140,6 +3194,17 @@ textarea.nodedc-field__control {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nodedc-inspector__section-icon,
|
||||
.nodedc-inspector__section-chevron {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nodedc-inspector__section-body {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.nodedc-inspector__section-trigger:not([data-open="true"]):hover {
|
||||
background: var(--nodedc-glass-control-hover);
|
||||
color: var(--nodedc-text-primary);
|
||||
@@ -3179,6 +3244,89 @@ textarea.nodedc-field__control {
|
||||
animation: nodedc-section-in var(--nodedc-duration-fast) ease;
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] {
|
||||
width: 100%;
|
||||
gap: 0.22rem;
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] :where(.nodedc-inspector__group, .nodedc-inspector__section) {
|
||||
gap: 0.22rem;
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-inspector__section {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-inspector__section-trigger {
|
||||
min-height: 3.55rem;
|
||||
grid-template-columns: 2.92rem minmax(0, 1fr) 1.4rem;
|
||||
align-items: center;
|
||||
align-content: initial;
|
||||
gap: 0.65rem;
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
background: var(--nodedc-panel-item-bg);
|
||||
color: var(--nodedc-text-secondary);
|
||||
padding: 5px 0.72rem 5px 5px;
|
||||
box-shadow: none;
|
||||
opacity: 0.72;
|
||||
transition: background 160ms ease, color 160ms ease, opacity 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-inspector__section-trigger:hover:not(:disabled) {
|
||||
background: var(--nodedc-panel-item-hover-bg);
|
||||
color: var(--nodedc-text-primary);
|
||||
opacity: 0.92;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-inspector__section-trigger:is([data-open="true"], [data-active="true"], [data-tone="accent"]) {
|
||||
background: var(--nodedc-panel-item-active-bg);
|
||||
color: var(--nodedc-text-primary);
|
||||
opacity: 1;
|
||||
box-shadow: inset 0 1px 0 color-mix(in srgb, white 12%, transparent), inset 0 -14px 26px color-mix(in srgb, var(--nodedc-canvas) 8%, transparent);
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-inspector__section-trigger:is([data-open="true"], [data-active="true"], [data-tone="accent"]) .nodedc-inspector__section-description {
|
||||
color: var(--nodedc-text-muted);
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-inspector__section-icon {
|
||||
display: grid;
|
||||
width: 2.92rem;
|
||||
height: 2.92rem;
|
||||
place-items: center;
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
background: var(--nodedc-panel-icon-bg);
|
||||
color: var(--nodedc-text-secondary);
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-inspector__section-trigger[data-open="true"] .nodedc-inspector__section-icon {
|
||||
background: var(--nodedc-panel-active-icon-bg);
|
||||
color: var(--nodedc-panel-active-icon-color);
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-inspector__section-chevron {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--nodedc-text-muted);
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-inspector__section-trigger[data-open="true"] .nodedc-inspector__section-chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-inspector__section-content {
|
||||
padding: 0.45rem 0.15rem 0.85rem;
|
||||
}
|
||||
|
||||
.nodedc-inspector[data-variant="panel"] .nodedc-control-row {
|
||||
width: 100%;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
gap: var(--nodedc-space-2);
|
||||
}
|
||||
|
||||
@keyframes nodedc-section-in {
|
||||
from { opacity: 0; transform: translateY(-3px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
@@ -3243,6 +3391,10 @@ textarea.nodedc-field__control {
|
||||
pointer-events: none;
|
||||
transform: translateX(calc(100vw + var(--nodedc-app-page-pad)));
|
||||
}
|
||||
|
||||
.nodedc-app-shell[data-content-open="true"][data-end-panel-open="true"] .nodedc-app-shell__content {
|
||||
right: calc(var(--nodedc-app-page-pad) + var(--nodedc-app-nav-width) + var(--nodedc-app-panel-gap));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
@@ -3286,13 +3438,19 @@ textarea.nodedc-field__control {
|
||||
}
|
||||
|
||||
.nodedc-app-shell__navigation,
|
||||
.nodedc-app-shell__content {
|
||||
.nodedc-app-shell__content,
|
||||
.nodedc-app-shell__end-panel {
|
||||
right: var(--nodedc-app-page-pad);
|
||||
bottom: var(--nodedc-app-page-pad);
|
||||
left: var(--nodedc-app-page-pad);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nodedc-app-shell[data-end-panel-open="true"] .nodedc-app-shell__content {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nodedc-app-shell[data-navigation-open="true"] .nodedc-app-shell__stage,
|
||||
.nodedc-app-shell[data-content-open="true"] .nodedc-app-shell__stage {
|
||||
padding-left: var(--nodedc-app-page-pad);
|
||||
@@ -3306,7 +3464,8 @@ textarea.nodedc-field__control {
|
||||
}
|
||||
|
||||
.nodedc-application-panel,
|
||||
.nodedc-admin-panel {
|
||||
.nodedc-admin-panel,
|
||||
.nodedc-application-side-panel {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
@@ -3400,6 +3559,7 @@ textarea.nodedc-field__control {
|
||||
.nodedc-window,
|
||||
.nodedc-workspace-window,
|
||||
.nodedc-application-panel,
|
||||
.nodedc-application-side-panel,
|
||||
.nodedc-inspector__section-content {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
Reference in New Issue
Block a user