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
+38 -1
View File
@@ -6,6 +6,7 @@ import {
AppHeader,
ApplicationPanel,
ApplicationShell,
ApplicationSidePanel,
Button,
Checker,
ColorField,
@@ -318,6 +319,9 @@ export function CatalogApp() {
const [mapTemplateSaveState, setMapTemplateSaveState] = useState<"idle" | "loading" | "saving" | "saved" | "error">("loading");
const mapTemplatePreviewRef = useRef<MapFixturePreviewHandle>(null);
const applicationMapPreviewRef = useRef<MapFixturePreviewHandle>(null);
const [mapSettingsPanelHost, setMapSettingsPanelHost] = useState<HTMLDivElement | null>(null);
const [mapHeaderActionsHost, setMapHeaderActionsHost] = useState<HTMLDivElement | null>(null);
const [mapSettingsPanelOpen, setMapSettingsPanelOpen] = useState(false);
const [createModuleOpen, setCreateModuleOpen] = useState(false);
const [createModuleName, setCreateModuleName] = useState("");
const [createModuleSlug, setCreateModuleSlug] = useState("");
@@ -355,6 +359,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 [sidePanelDemoOpen, setSidePanelDemoOpen] = useState(true);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [shareAccessOpen, setShareAccessOpen] = useState(false);
@@ -1413,6 +1418,23 @@ export function CatalogApp() {
</div>
<p className="catalog-preview__explanation">Rectangle, maximize, close и stacking остаются состоянием приложения; дизайн-система владеет одинаковой bounded-геометрией и доступным управлением.</p>
</Preview>
<Preview title="Application side panel" note="end / push / controlled" className="catalog-preview--wide catalog-side-panel-preview">
<div className="catalog-side-panel-demo">
{sidePanelDemoOpen ? (
<ApplicationSidePanel
eyebrow="MAP / SETTINGS"
title="Настройки карты"
description="Theme-aware application panel"
onClose={() => setSidePanelDemoOpen(false)}
>
<Inspector variant="panel" sections={environmentSections.slice(0, 3)} defaultOpen={[environmentSections[0]?.id ?? ""]} singleOpen />
</ApplicationSidePanel>
) : (
<Button shape="pill" icon={<Icon name="panel" />} onClick={() => setSidePanelDemoOpen(true)}>Открыть правую панель</Button>
)}
</div>
<p className="catalog-preview__explanation">Панель занимает отдельную end-колонку ApplicationShell, сдвигает рабочую область, наследует тему и не входит в z-index stack окон карты.</p>
</Preview>
<Preview title="Inspector" note="modeless / draggable" className="catalog-preview--wide catalog-preview--compact catalog-inspector-launcher">
<p className="catalog-preview__explanation">Настройки приложения и Toolbar открываются в отдельном перемещаемом Inspector и сохраняются одним серверным layout.</p>
<Button variant="primary" shape="pill" icon={<Icon name="panel" />} onClick={() => setInspectorOpen(true)}>Открыть Inspector</Button>
@@ -1794,6 +1816,9 @@ export function CatalogApp() {
expanded
initialLayout={mapDesignLayout}
features={Object.fromEntries(template.features.map((feature) => [feature.id, feature.required ? true : feature.defaultVisible]))}
settingsPanelHost={mapSettingsPanelHost}
headerActionsHost={mapHeaderActionsHost}
onSettingsPanelOpenChange={setMapSettingsPanelOpen}
/>
<div className="catalog-page-template__actions">
<Button variant="primary" shape="pill" icon={<Icon name="plus" />} onClick={() => { void createApplicationDraft(template); }}>Создать Application Draft</Button>
@@ -1807,7 +1832,12 @@ export function CatalogApp() {
title={template.title}
description={template.description}
>
<MapFixturePreview features={Object.fromEntries(template.features.map((feature) => [feature.id, feature.required ? true : feature.defaultVisible]))} />
<MapFixturePreview
features={Object.fromEntries(template.features.map((feature) => [feature.id, feature.required ? true : feature.defaultVisible]))}
settingsPanelHost={mapSettingsPanelHost}
headerActionsHost={mapHeaderActionsHost}
onSettingsPanelOpenChange={setMapSettingsPanelOpen}
/>
<div className="catalog-page-template__actions">
<Button variant="primary" shape="pill" icon={<Icon name="plus" />} onClick={() => { void createApplicationDraft(template); }}>Создать Application Draft</Button>
<span>Только утверждённые features и slots без свободного canvas.</span>
@@ -1860,6 +1890,9 @@ export function CatalogApp() {
initialLayout={resolvedMapLayout}
features={page.features}
expanded
settingsPanelHost={mapSettingsPanelHost}
headerActionsHost={mapHeaderActionsHost}
onSettingsPanelOpenChange={setMapSettingsPanelOpen}
/>
{applicationMode === "edit" ? (
<SettingsCard eyebrow="PAGE SETTINGS" title={page.title} description={`${template.title} · ${template.version}`}>
@@ -1888,6 +1921,8 @@ export function CatalogApp() {
navigationOpen={guidelineOpen}
contentOpen={workspace.contentOpen}
contentExpanded={panelExpanded}
endPanelOpen={mapSettingsPanelOpen}
endPanel={<div ref={setMapSettingsPanelHost} className="catalog-map-settings-panel-host" />}
header={
<AppHeader
brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />}
@@ -2035,6 +2070,7 @@ export function CatalogApp() {
description={`${activePageTemplate.category} · contract ${activePageTemplate.schemaVersion} · template ${activePageTemplate.version}`}
expanded={panelExpanded}
onExpandedChange={workspace.setContentExpanded}
headerTools={activePageTemplate.id === "map" ? <div ref={setMapHeaderActionsHost} className="catalog-map-header-actions-host" /> : undefined}
utilityActions={activePageTemplate.id === "map" ? [{
label: mapTemplateSaveState === "saving" ? "Сохраняем Map-фрагмент в Design Profile" : mapTemplateSaveState === "error" ? "Повторить сохранение Map-фрагмента" : "Сохранить дизайн Map в Design Profile",
icon: "save",
@@ -2053,6 +2089,7 @@ export function CatalogApp() {
description={activeApplicationPage ? `${activeApplication.metadata.name} · ${activeApplicationPage.template.id}@${activeApplicationPage.template.version}` : `/${activeApplication.metadata.slug} · manifest ${activeApplication.schemaVersion}`}
expanded={panelExpanded}
onExpandedChange={workspace.setContentExpanded}
headerTools={activeApplicationPage?.template.id === "map" ? <div ref={setMapHeaderActionsHost} className="catalog-map-header-actions-host" /> : undefined}
utilityActions={[{
label: applicationSaveState === "saving" ? "Сохранение…" : applicationSaveState === "error" ? "Повторить сохранение" : "Сохранить",
icon: "save",
-4
View File
@@ -3080,8 +3080,6 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
const picked = viewer?.scene.pick(movement.position);
const pickedId = picked?.id;
if (pickedId instanceof Entity && pickedId.id && !String(pickedId.id).startsWith("grid/")) {
gridController?.setSelection(null);
onGridSectorSelectRef.current?.(null);
onSelectRef.current?.(pickedId.id);
return;
}
@@ -3091,8 +3089,6 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
&& (pickedId as Partial<HGeoZonePickId>).kind === "nodedc-hgeozone"
&& typeof (pickedId as Partial<HGeoZonePickId>).entityId === "string"
) {
gridController?.setSelection(null);
onGridSectorSelectRef.current?.(null);
onSelectRef.current?.((pickedId as HGeoZonePickId).entityId);
return;
}
+114 -137
View File
@@ -1,5 +1,6 @@
import { forwardRef, lazy, Suspense, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent, type PointerEvent } from "react";
import { Button, Checker, ColorField, ControlRow, Dropdown, Icon, IconButton, Inspector, InspectorSelectField, RangeControl, SegmentedControl, WorkspaceWindow } from "@nodedc/ui-react";
import { createPortal } from "react-dom";
import { ApplicationSidePanel, Button, Checker, ColorField, ControlRow, Dropdown, Icon, IconButton, Inspector, InspectorSelectField, RangeControl, SegmentedControl, WorkspaceWindow } from "@nodedc/ui-react";
import type { SelectOption, WorkspaceWindowRect } from "@nodedc/ui-react";
import type {
CameraSpiralState,
@@ -461,7 +462,7 @@ export type MapSubjectWindowState = {
zIndex: number;
};
type MapWorkspaceWindowId = "settings" | "layers" | "sector" | "subject-card" | `binding:${string}`;
type MapWorkspaceWindowId = "sector" | "subject-card" | `binding:${string}`;
export type MapSubjectState = {
bindingId: string;
@@ -641,20 +642,6 @@ function defaultSubjectWindowState(index: number): MapSubjectWindowState {
};
}
const defaultLayersWindowRect: WorkspaceWindowRect = {
x: 24,
y: 72,
width: 336,
height: 500,
};
const defaultSettingsWindowRect: WorkspaceWindowRect = {
x: 930,
y: 72,
width: 420,
height: 530,
};
const defaultSectorWindowRect: WorkspaceWindowRect = {
x: 24,
y: 72,
@@ -718,7 +705,10 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
initialLayout?: MapPageLayout | null;
applicationId?: string;
pageId?: string;
}>(function MapFixturePreview({ features = { inspector: true, toolbar: true, assistant: false }, expanded = false, initialLayout = null, applicationId, pageId }, ref) {
settingsPanelHost?: HTMLElement | null;
headerActionsHost?: HTMLElement | null;
onSettingsPanelOpenChange?: (open: boolean) => void;
}>(function MapFixturePreview({ features = { inspector: true, toolbar: true, assistant: false }, expanded = false, initialLayout = null, applicationId, pageId, settingsPanelHost, headerActionsHost, onSettingsPanelOpenChange }, ref) {
const workspaceRef = useRef<HTMLDivElement>(null);
const [selectedId, setSelectedId] = useState<string>();
const [selectedGridSector, setSelectedGridSector] = useState<GridSectorSelection | null>(null);
@@ -730,16 +720,10 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
const [subjectCardTabId, setSubjectCardTabId] = useState("overview");
const [expandedFacetRows, setExpandedFacetRows] = useState<Record<string, boolean>>({});
const [inspectorOpen, setInspectorOpen] = useState(false);
const [settingsWindowRect, setSettingsWindowRect] = useState<WorkspaceWindowRect>(defaultSettingsWindowRect);
const [settingsWindowMaximized, setSettingsWindowMaximized] = useState(false);
const [settingsWindowZIndex, setSettingsWindowZIndex] = useState(13);
const [inspectorOpenSections, setInspectorOpenSections] = useState<string[]>(() => (
initialLayout?.inspectorOpenSections ?? ["map-base"]
));
const [layersOpen, setLayersOpen] = useState(false);
const [layersWindowRect, setLayersWindowRect] = useState<WorkspaceWindowRect>(defaultLayersWindowRect);
const [layersWindowMaximized, setLayersWindowMaximized] = useState(false);
const [layersWindowZIndex, setLayersWindowZIndex] = useState(12);
const [sectorWindowRect, setSectorWindowRect] = useState<WorkspaceWindowRect>(defaultSectorWindowRect);
const [sectorWindowMaximized, setSectorWindowMaximized] = useState(false);
const [sectorWindowZIndex, setSectorWindowZIndex] = useState(142);
@@ -1385,15 +1369,11 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
if (activeWorkspaceWindowId === windowId) return;
const nextZIndex = Math.max(
20,
settingsWindowZIndex,
layersWindowZIndex,
sectorWindowZIndex,
subjectCardZIndex,
...Object.values(subjectStates).map((state) => state.window.zIndex),
) + 1;
if (windowId === "settings") setSettingsWindowZIndex(nextZIndex);
else if (windowId === "layers") setLayersWindowZIndex(nextZIndex);
else if (windowId === "sector") setSectorWindowZIndex(nextZIndex);
if (windowId === "sector") setSectorWindowZIndex(nextZIndex);
else if (windowId === "subject-card") setSubjectCardZIndex(nextZIndex);
else if (windowId.startsWith("binding:")) {
const bindingId = windowId.slice("binding:".length);
@@ -1403,7 +1383,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
}));
}
setActiveWorkspaceWindowId(windowId);
}, [activeWorkspaceWindowId, layersWindowZIndex, sectorWindowZIndex, settingsWindowZIndex, subjectCardZIndex, subjectStates, updateSubjectState]);
}, [activeWorkspaceWindowId, sectorWindowZIndex, subjectCardZIndex, subjectStates, updateSubjectState]);
const clearActiveWorkspaceWindow = (windowId: MapWorkspaceWindowId) => {
setActiveWorkspaceWindowId((current) => current === windowId ? undefined : current);
@@ -1422,24 +1402,15 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
clearActiveWorkspaceWindow(`binding:${bindingId}`);
};
const activateLayersWindow = () => {
activateWorkspaceWindow("layers");
};
const closeSettingsPanel = useCallback(() => setInspectorOpen(false), []);
const toggleLayersWindow = () => {
if (layersOpen) {
setLayersOpen(false);
clearActiveWorkspaceWindow("layers");
return;
}
setLayersOpen(true);
activateLayersWindow();
};
const toggleSettingsPanel = () => setInspectorOpen((current) => !current);
const openSettingsWindow = () => {
setInspectorOpen(true);
activateWorkspaceWindow("settings");
};
useEffect(() => {
onSettingsPanelOpenChange?.(inspectorOpen && Boolean(features.inspector));
}, [features.inspector, inspectorOpen, onSettingsPanelOpenChange]);
useEffect(() => () => onSettingsPanelOpenChange?.(false), [onSettingsPanelOpenChange]);
const deactivateGridSector = () => {
setSelectedGridSector(null);
@@ -1718,6 +1689,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label: "Подложка и terrain",
description: "provider-neutral surface",
group: "Карта",
icon: <Icon name="globe" />,
content: <>
<ControlRow label="Подложка"><strong>Cesium World Imagery</strong></ControlRow>
<small className="catalog-map-inspector__note">Текущий официальный provider. Другие provider-слои появятся только после отдельного asset-контракта Platform.</small>
@@ -1743,6 +1715,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label: "Атмосфера и освещение",
description: "scene / color correction",
group: "Карта",
icon: <Icon name="activity" />,
content: <>
<Checker checked={mapSettings.atmosphereEnabled} label="Показывать атмосферу" onChange={(atmosphereEnabled) => updateMapSettings({ atmosphereEnabled })} />
<RangeControl label="Атмосфера: оттенок" value={mapSettings.atmosphereHue} min={-100} max={100} formatValue={(value) => `${value}%`} onChange={(atmosphereHue) => updateMapSettings({ atmosphereHue })} />
@@ -1761,6 +1734,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label: "3D здания",
description: "3D Tiles / detail",
group: "Карта",
icon: <Icon name="building" />,
content: <>
<Checker checked={mapSettings.buildingsVisible} label="Показывать 3D здания" onChange={(buildingsVisible) => updateMapSettings({ buildingsVisible })} />
<ControlRow label="Цвет"><ColorField label="Цвет зданий" value={mapSettings.buildingsColor} onChange={(buildingsColor) => updateMapSettings({ buildingsColor })} /></ControlRow>
@@ -1777,6 +1751,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label: referenceProfile ? profile.title : profile.target.variant === "surface-fill" ? "HGeoZone" : "Таргет",
description: profile.target.variant === "surface-fill" ? `проекция · ${profile.title}` : profile.title,
group: referenceProfile ? "Станции" : profile.target.variant === "surface-fill" ? "Слои" : "Таргеты",
icon: <Icon name={referenceProfile ? "globe" : profile.target.variant === "surface-fill" ? "grid" : "target"} />,
content: <>
<small className="catalog-map-inspector__note">Профиль принадлежит этой странице Application и управляется тем же provider-neutral MCP-контрактом. Исходный API в настройках отсутствует.</small>
{referenceLayer ? (
@@ -1831,6 +1806,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label: "Классы состояния",
description: "нормализованные фасеты онтологии",
group: "Таргеты",
icon: <Icon name="sliders" />,
content: <>
<small className="catalog-map-inspector__note">Цвета назначены семантическим классам после нормализации данных. Здесь нет названий provider-статусов и привязки к транспорту.</small>
{profile.styles.map((style) => {
@@ -1850,6 +1826,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label: "Сетка и LOD",
description: "first adapter control",
group: "Слои",
icon: <Icon name="grid" />,
content: <div className="catalog-map-grid-inspector">
<small className="catalog-map-inspector__note">Фиксированная московская ENU-адресация задаёт неизменные сектора на LOD 13. LOD 45 используют глобальную WGS84-гратику́лу; камера выбирает только LOD и видимую область.</small>
<Checker checked={mapSettings.gridVisible} label="Сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
@@ -2108,6 +2085,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label: "Анимация камеры",
description: "geodesic spiral survey",
group: "Камера",
icon: <Icon name="activity" />,
content: <>
<Checker checked={animationModeEnabled} label="Режим анимации" onChange={setAnimationMode} />
{animationModeEnabled ? <>
@@ -2186,6 +2164,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label: "TileCache",
description: "Platform Map Gateway",
group: "Хранение",
icon: <Icon name="database" />,
content: <>
<small className="catalog-map-inspector__note">Общий persistent cache Platform: он не принадлежит приложению, странице или пользователю.</small>
<Checker checked={mapSettings.cacheEnabled} label="Кэшировать live-данные" onChange={setCacheEnabled} />
@@ -2211,6 +2190,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label: "Выбранная сущность",
description: "selection contract",
group: "Данные",
icon: <Icon name="target" />,
content: <>
<ControlRow label="Сущность"><strong>{selected?.title ?? "Нет выбора"}</strong></ControlRow>
<ControlRow label="Тип"><span>{selected?.kind ?? "—"}{selected?.status ? ` · ${selected.status}` : ""}</span></ControlRow>
@@ -2218,6 +2198,31 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
},
];
const headerActions = (
<div className="catalog-map-header-actions" aria-label="Действия карты">
<IconButton className="catalog-map-header-action" label="Настройки карты" aria-pressed={inspectorOpen} data-active={inspectorOpen || undefined} onClick={toggleSettingsPanel}><Icon name="settings" /></IconButton>
{features.toolbar ? <IconButton className="catalog-map-header-action" label="Toolbar" aria-pressed={toolbarOpen} data-active={toolbarOpen || undefined} onClick={() => setToolbarOpen((value) => !value)}><Icon name="panel" /></IconButton> : null}
</div>
);
const settingsPanel = inspectorOpen && Boolean(features.inspector) ? (
<ApplicationSidePanel
eyebrow="MAP / SETTINGS"
title="Настройки карты"
description="Application-owned layout"
onClose={closeSettingsPanel}
aria-label="Настройки карты"
>
<Inspector
variant="panel"
sections={inspectorSections}
openSections={inspectorOpenSections}
singleOpen
onOpenSectionsChange={setInspectorOpenSections}
/>
</ApplicationSidePanel>
) : null;
return (
<div
ref={workspaceRef}
@@ -2225,71 +2230,33 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
style={{ "--catalog-map-height": `${mapHeight}px` } as CSSProperties}
aria-label="Map Page Cesium adapter"
>
<Suspense fallback={<div className="catalog-map-fixture__loading">Загрузка карты</div>}>
<CesiumMapRenderer
key={rendererRevision}
ref={mapRendererRef}
onSelect={handleSelect}
onGridSectorSelect={handleGridSectorSelect}
selectedGridSector={selectedGridSector}
onGatewayHealth={handleRendererGatewayHealth}
onProviderStatus={setProviderStatus}
onCameraChange={handleCameraChange}
onCacheRefreshConsumed={handleCacheRefreshConsumed}
onReadyChange={setMapRendererReady}
onSpiralStateChange={handleSpiralStateChange}
initialCamera={mapCamera ?? undefined}
presentation={presentation}
runtimeBindings={[...sectorScopedPrimaryRuntimeBindings, ...referenceRuntimeBindings]}
presentationProfiles={presentationProfiles}
presentationFilters={rendererPresentationFilters}
/>
</Suspense>
<div className="catalog-map-fixture__actions">
<IconButton label="Настройки карты" aria-pressed={inspectorOpen} data-active={inspectorOpen || undefined} onClick={openSettingsWindow}><Icon name="settings" /></IconButton>
<IconButton label="Слои карты" aria-pressed={layersOpen} data-active={layersOpen || undefined} onClick={toggleLayersWindow}><Icon name="grid" /></IconButton>
{features.toolbar ? <IconButton label="Toolbar" aria-pressed={toolbarOpen} data-active={toolbarOpen || undefined} onClick={() => setToolbarOpen((value) => !value)}><Icon name="panel" /></IconButton> : null}
{features.assistant ? <IconButton label="Assistant" aria-pressed={assistantOpen} data-active={assistantOpen || undefined} onClick={() => setAssistantOpen((value) => !value)}><Icon name="apps" /></IconButton> : null}
<div className="catalog-map-fixture__renderer">
<Suspense fallback={<div className="catalog-map-fixture__loading">Загрузка карты</div>}>
<CesiumMapRenderer
key={rendererRevision}
ref={mapRendererRef}
onSelect={handleSelect}
onGridSectorSelect={handleGridSectorSelect}
selectedGridSector={selectedGridSector}
onGatewayHealth={handleRendererGatewayHealth}
onProviderStatus={setProviderStatus}
onCameraChange={handleCameraChange}
onCacheRefreshConsumed={handleCacheRefreshConsumed}
onReadyChange={setMapRendererReady}
onSpiralStateChange={handleSpiralStateChange}
initialCamera={mapCamera ?? undefined}
presentation={presentation}
runtimeBindings={[...sectorScopedPrimaryRuntimeBindings, ...referenceRuntimeBindings]}
presentationProfiles={presentationProfiles}
presentationFilters={rendererPresentationFilters}
/>
</Suspense>
</div>
{layersOpen ? (
<WorkspaceWindow
boundsRef={workspaceRef}
rect={layersWindowRect}
onRectChange={setLayersWindowRect}
maximized={layersWindowMaximized}
onMaximizedChange={setLayersWindowMaximized}
onActivate={activateLayersWindow}
onClose={() => {
setLayersOpen(false);
clearActiveWorkspaceWindow("layers");
}}
title="Слои карты"
active={activeWorkspaceWindowId === "layers"}
zIndex={layersWindowZIndex}
minWidth={320}
minHeight={360}
className="catalog-map-fixture__layers catalog-map-fixture__map-glass-window"
aria-label="Настройки слоёв карты"
>
<div className="catalog-map-fixture__layers-content">
<div className="catalog-map-fixture__provider">
<strong>Cesium World Imagery</strong>
<small>официальный live provider · imagery: {providerStateLabel[providerStatus.imagery]} · terrain: {providerStateLabel[providerStatus.terrain]}</small>
</div>
{Object.entries(providerStatus.errors).map(([provider, error]) => <small key={provider} className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{provider}: {error}</small>)}
<Checker checked={mapSettings.terrainEnabled} label="Terrain" onChange={(terrainEnabled) => updateMapSettings({ terrainEnabled })} />
<Checker checked={mapSettings.buildingsVisible} label="3D здания" onChange={(buildingsVisible) => updateMapSettings({ buildingsVisible })} />
<Checker checked={mapSettings.gridVisible} label="Планетарная сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
<small className="catalog-map-inspector__note">Live Cache: {liveCacheSummary}</small>
{transportDiagnostic ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{transportDiagnostic}</small> : null}
{gatewayHealthAge ? <small className="catalog-map-inspector__note">{gatewayHealthAge}</small> : null}
{gatewayCheckState === "error" || gatewayCheckState === "stale" ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{gatewayCheckError}</small> : null}
<Checker className="catalog-map-fixture__cache-toggle" checked={mapSettings.cacheEnabled} label="Кэшировать live-данные" onChange={setCacheEnabled} />
<Checker className="catalog-map-fixture__cache-toggle" checked={mapSettings.cacheNoOverwrite} disabled={!mapSettings.cacheEnabled} label="Не перезаписывать cache" onChange={setCacheNoOverwrite} />
</div>
</WorkspaceWindow>
{features.assistant ? (
<div className="catalog-map-fixture__actions">
<IconButton label="Assistant" aria-pressed={assistantOpen} data-active={assistantOpen || undefined} onClick={() => setAssistantOpen((value) => !value)}><Icon name="apps" /></IconButton>
</div>
) : null}
{selectedGridSector ? (
@@ -2497,6 +2464,39 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
</div>
)}
</Dropdown>
<Dropdown
placement="top-start"
width={320}
minWidth={240}
offset={10}
surfaceRole="dialog"
surfaceClassName="catalog-map-fixture__objects-menu catalog-map-fixture__layers-menu nodedc-map-glass"
onOpenChange={setLayersOpen}
trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
<IconButton ref={setTriggerRef} label="Слои карты" aria-controls={surfaceId} aria-expanded={open} aria-pressed={open} data-active={open || undefined} onClick={toggle}><Icon name="grid" /></IconButton>
)}
>
<div className="catalog-map-fixture__layers-content" aria-label="Настройки слоёв карты">
<div className="catalog-map-fixture__objects-menu-head">
<strong>Слои карты</strong>
<small>Подложка, рельеф и визуальные слои</small>
</div>
<div className="catalog-map-fixture__provider">
<strong>Cesium World Imagery</strong>
<small>официальный live provider · imagery: {providerStateLabel[providerStatus.imagery]} · terrain: {providerStateLabel[providerStatus.terrain]}</small>
</div>
{Object.entries(providerStatus.errors).map(([provider, error]) => <small key={provider} className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{provider}: {error}</small>)}
<Checker checked={mapSettings.terrainEnabled} label="Terrain" onChange={(terrainEnabled) => updateMapSettings({ terrainEnabled })} />
<Checker checked={mapSettings.buildingsVisible} label="3D здания" onChange={(buildingsVisible) => updateMapSettings({ buildingsVisible })} />
<Checker checked={mapSettings.gridVisible} label="Планетарная сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
<small className="catalog-map-inspector__note">Live Cache: {liveCacheSummary}</small>
{transportDiagnostic ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{transportDiagnostic}</small> : null}
{gatewayHealthAge ? <small className="catalog-map-inspector__note">{gatewayHealthAge}</small> : null}
{gatewayCheckState === "error" || gatewayCheckState === "stale" ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{gatewayCheckError}</small> : null}
<Checker className="catalog-map-fixture__cache-toggle" checked={mapSettings.cacheEnabled} label="Кэшировать live-данные" onChange={setCacheEnabled} />
<Checker className="catalog-map-fixture__cache-toggle" checked={mapSettings.cacheNoOverwrite} disabled={!mapSettings.cacheEnabled} label="Не перезаписывать cache" onChange={setCacheNoOverwrite} />
</div>
</Dropdown>
<IconButton label="Обзор объектов" onClick={() => mapRendererRef.current?.fitRuntimeEntities(visibleTargetEntityIds)}><Icon name="globe" /></IconButton>
<IconButton
label={searchOpen ? "Закрыть поиск" : "Поиск"}
@@ -2741,35 +2741,12 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
{assistantOpen ? <div className="catalog-map-fixture__assistant"><strong>NODE.DC Assistant</strong><span>Контекст выбранной сущности готов к передаче.</span></div> : null}
<button type="button" className="catalog-map-fixture__resize" aria-label="Изменить высоту карты" onPointerDown={startResize}><span /></button>
{inspectorOpen && Boolean(features.inspector) ? (
<WorkspaceWindow
boundsRef={workspaceRef}
rect={settingsWindowRect}
onRectChange={setSettingsWindowRect}
maximized={settingsWindowMaximized}
onMaximizedChange={setSettingsWindowMaximized}
onActivate={() => activateWorkspaceWindow("settings")}
onClose={() => {
setInspectorOpen(false);
clearActiveWorkspaceWindow("settings");
}}
title="Настройки карты"
subtitle="MAP / inspector"
active={activeWorkspaceWindowId === "settings"}
zIndex={settingsWindowZIndex}
minWidth={360}
minHeight={380}
className="catalog-map-fixture__map-settings-window catalog-map-fixture__map-glass-window"
aria-label="Настройки карты"
>
<Inspector
sections={inspectorSections}
openSections={inspectorOpenSections}
singleOpen
onOpenSectionsChange={setInspectorOpenSections}
/>
</WorkspaceWindow>
) : null}
{settingsPanel
? settingsPanelHost
? createPortal(settingsPanel, settingsPanelHost)
: <div className="catalog-map-fixture__settings-panel-fallback">{settingsPanel}</div>
: null}
{headerActionsHost ? createPortal(headerActions, headerActionsHost) : null}
</div>
);
});
+83 -16
View File
@@ -568,6 +568,11 @@ textarea {
.catalog-map-fixture--expanded { min-height: 22.5rem; }
.catalog-map-fixture__renderer {
position: absolute;
inset: 0;
}
.catalog-map-fixture__loading {
position: absolute;
inset: 0;
@@ -612,6 +617,36 @@ textarea {
gap: 0.45rem;
}
.catalog-map-header-actions-host,
.catalog-map-header-actions {
display: flex;
align-items: center;
gap: 0.45rem;
}
.catalog-map-header-action.nodedc-icon-button {
width: 2.92rem;
height: 2.92rem;
flex-basis: 2.92rem;
border: 1px solid var(--nodedc-panel-action-border);
background: var(--nodedc-panel-action-bg);
color: var(--nodedc-text-secondary);
box-shadow: var(--nodedc-panel-action-shadow);
}
.catalog-map-header-action.nodedc-icon-button:hover,
.catalog-map-header-action.nodedc-icon-button:focus-visible {
border-color: var(--nodedc-panel-action-border-hover);
background: var(--nodedc-glass-control-hover);
color: var(--nodedc-text-primary);
}
.catalog-map-header-action.nodedc-icon-button[data-active="true"] {
border-color: transparent;
background: var(--nodedc-glass-control-active);
color: var(--nodedc-glass-control-active-text);
}
.catalog-map-fixture__actions .nodedc-icon-button,
.catalog-map-fixture__toolbar .nodedc-icon-button {
border: 0;
@@ -633,14 +668,11 @@ textarea {
color: var(--nodedc-glass-control-active-text);
}
.catalog-map-fixture__layers,
.catalog-map-fixture__map-settings-window,
.catalog-map-fixture__sector-window {
--nodedc-radius-modal: 1.45rem;
}
.catalog-map-fixture__map-glass-window,
.catalog-map-fixture__map-settings-window {
.catalog-map-fixture__map-glass-window {
--nodedc-canvas: #111216;
--nodedc-text-primary: rgba(13, 14, 17, 0.96);
--nodedc-text-secondary: rgba(13, 14, 17, 0.72);
@@ -667,10 +699,6 @@ textarea {
color: #ffffff;
}
.catalog-map-fixture__map-settings-window .nodedc-workspace-window__body {
padding: 0.3rem 0.75rem 0.8rem;
}
.catalog-map-fixture__sector-window .nodedc-workspace-window__body {
padding: 0.25rem 0.72rem 0.7rem;
}
@@ -765,12 +793,17 @@ textarea {
gap: 0.55rem;
}
.catalog-map-fixture__layers .nodedc-workspace-window__body {
padding-top: 0.35rem;
.catalog-map-fixture__layers-menu .nodedc-checker {
min-height: 3rem;
}
.catalog-map-fixture__layers .nodedc-checker {
min-height: 3rem;
.catalog-map-fixture__layers-menu {
--nodedc-text-primary: var(--nodedc-map-glass-text);
--nodedc-text-secondary: var(--nodedc-map-glass-text);
--nodedc-text-muted: var(--nodedc-map-glass-text-muted);
--nodedc-glass-control-bg: rgb(255 255 255 / 0.14);
--nodedc-glass-control-hover: rgb(255 255 255 / 0.22);
--nodedc-field-bg: rgb(255 255 255 / 0.14);
}
.catalog-map-fixture__provider {
@@ -796,7 +829,7 @@ textarea {
bottom: 1rem;
left: 50%;
display: flex;
width: 10.8rem;
width: 13.45rem;
gap: 0.35rem;
border-radius: var(--nodedc-radius-circle);
background: var(--nodedc-map-glass-bg);
@@ -809,6 +842,7 @@ textarea {
.catalog-map-fixture__toolbar[data-search-open] {
width: min(42rem, calc(100% - 2rem));
padding-right: 0;
}
.catalog-map-search {
@@ -822,7 +856,7 @@ textarea {
}
.catalog-map-search[data-open] {
flex-basis: 28rem;
flex: 1 1 0;
overflow: visible;
opacity: 1;
pointer-events: auto;
@@ -837,7 +871,7 @@ textarea {
border-radius: var(--nodedc-radius-circle);
background: rgb(255 255 255 / 0.84);
padding: 0 0.82rem;
color: var(--nodedc-map-glass-text);
color: var(--nodedc-map-field-text);
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.34);
}
@@ -857,7 +891,7 @@ textarea {
}
.catalog-map-search__field input::placeholder {
color: var(--nodedc-map-glass-text-muted);
color: var(--nodedc-map-field-text-muted);
}
.catalog-map-search__field input::-webkit-search-cancel-button {
@@ -938,6 +972,13 @@ textarea {
padding: 0.48rem;
}
.catalog-map-fixture__settings-panel-fallback {
position: absolute;
z-index: 500;
inset: 0 0 0 auto;
width: min(22rem, calc(100% - 1rem));
}
.catalog-map-fixture__objects-menu-list,
.catalog-map-fixture__objects-menu-head,
.catalog-map-fixture__objects-menu-toggle {
@@ -1630,6 +1671,32 @@ textarea {
transform: translate(-50%, -50%);
}
.catalog-side-panel-preview .catalog-preview__body {
align-content: stretch;
}
.catalog-side-panel-demo {
display: grid;
min-height: 31rem;
justify-content: end;
align-items: stretch;
overflow: hidden;
border-radius: var(--nodedc-radius-card);
background:
radial-gradient(circle at 30% 24%, color-mix(in srgb, var(--catalog-accent) 20%, transparent), transparent 36%),
var(--nodedc-nested-surface);
padding-left: 1rem;
}
.catalog-side-panel-demo > .nodedc-application-side-panel {
width: min(22rem, 100%);
}
.catalog-side-panel-demo > .nodedc-button {
align-self: center;
justify-self: center;
}
.catalog-inspector-launcher .nodedc-button {
width: 100%;
}