feat(foundry): refine map application controls
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 1–3. LOD 4–5 используют глобальную 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
@@ -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%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user