fix(map): align canonical map actions
This commit is contained in:
@@ -39,19 +39,37 @@
|
|||||||
max-width: min(58%, 42rem);
|
max-width: min(58%, 42rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mission-map__toolbar {
|
.mission-map__map-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 4;
|
z-index: 5;
|
||||||
right: 50%;
|
top: 1rem;
|
||||||
bottom: 1rem;
|
right: 1rem;
|
||||||
left: auto;
|
display: flex;
|
||||||
transform: translateX(50%);
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-map__map-action.nodedc-icon-button {
|
||||||
|
background: var(--nodedc-map-glass-bg);
|
||||||
|
color: var(--nodedc-map-glass-text);
|
||||||
|
box-shadow: var(--nodedc-glass-dropdown-shadow);
|
||||||
|
backdrop-filter: blur(var(--nodedc-blur-control));
|
||||||
|
-webkit-backdrop-filter: blur(var(--nodedc-blur-control));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-map__map-action.nodedc-icon-button:hover:not(:disabled) {
|
||||||
|
background: var(--nodedc-map-glass-hover);
|
||||||
|
color: var(--nodedc-map-glass-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-map__map-action.nodedc-icon-button[aria-pressed="true"] {
|
||||||
|
background: var(--nodedc-map-glass-active);
|
||||||
|
color: var(--nodedc-glass-control-active-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mission-map__layers {
|
.mission-map__layers {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
top: 1rem;
|
top: calc(1rem + var(--nodedc-icon-button-size) + 0.6rem);
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
width: min(23rem, calc(100% - 2rem));
|
width: min(23rem, calc(100% - 2rem));
|
||||||
@@ -180,8 +198,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mission-map__layers {
|
.mission-map__layers {
|
||||||
top: 0.65rem;
|
top: calc(0.65rem + var(--nodedc-icon-button-size) + 0.6rem);
|
||||||
right: 0.65rem;
|
right: 0.65rem;
|
||||||
width: calc(100% - 1.3rem);
|
width: calc(100% - 1.3rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mission-map__map-actions {
|
||||||
|
top: 0.65rem;
|
||||||
|
right: 0.65rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
@@ -24,7 +23,6 @@ import {
|
|||||||
MapGlassSurface,
|
MapGlassSurface,
|
||||||
RangeControl,
|
RangeControl,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
Toolbar,
|
|
||||||
Window,
|
Window,
|
||||||
WindowFooterActions,
|
WindowFooterActions,
|
||||||
} from "@nodedc/ui-react";
|
} from "@nodedc/ui-react";
|
||||||
@@ -48,8 +46,6 @@ const editableInspectorSections = new Set<MapInspectorSection>([
|
|||||||
"tile-cache",
|
"tile-cache",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type ToolbarAction = "settings" | "layers" | "reset-view";
|
|
||||||
|
|
||||||
export function WorldMapWorkspace({
|
export function WorldMapWorkspace({
|
||||||
definition,
|
definition,
|
||||||
}: {
|
}: {
|
||||||
@@ -105,15 +101,6 @@ export function WorldMapWorkspace({
|
|||||||
}
|
}
|
||||||
}, [controller, draft]);
|
}, [controller, draft]);
|
||||||
|
|
||||||
const resetRenderer = useCallback(() => {
|
|
||||||
operatorCameraInteraction.current = false;
|
|
||||||
updateDraft((current) => {
|
|
||||||
current.view.camera = null;
|
|
||||||
return current;
|
|
||||||
});
|
|
||||||
setRendererGeneration((generation) => generation + 1);
|
|
||||||
}, [updateDraft]);
|
|
||||||
|
|
||||||
const retryRenderer = useCallback(() => {
|
const retryRenderer = useCallback(() => {
|
||||||
operatorCameraInteraction.current = false;
|
operatorCameraInteraction.current = false;
|
||||||
setRendererGeneration((generation) => generation + 1);
|
setRendererGeneration((generation) => generation + 1);
|
||||||
@@ -137,35 +124,6 @@ export function WorldMapWorkspace({
|
|||||||
});
|
});
|
||||||
}, [updateDraft]);
|
}, [updateDraft]);
|
||||||
|
|
||||||
const toolbarItems = useMemo(() => [
|
|
||||||
{
|
|
||||||
id: "settings" as const,
|
|
||||||
label: "Настройки карты",
|
|
||||||
icon: "settings" as const,
|
|
||||||
active: settingsOpen,
|
|
||||||
onSelect: () => {
|
|
||||||
setSettingsOpen((open) => !open);
|
|
||||||
setLayersOpen(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "layers" as const,
|
|
||||||
label: "Слои карты",
|
|
||||||
icon: "grid" as const,
|
|
||||||
active: layersOpen,
|
|
||||||
onSelect: () => {
|
|
||||||
setLayersOpen((open) => !open);
|
|
||||||
setSettingsOpen(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "reset-view" as const,
|
|
||||||
label: "Сбросить камеру",
|
|
||||||
icon: "target" as const,
|
|
||||||
onSelect: resetRenderer,
|
|
||||||
},
|
|
||||||
], [layersOpen, resetRenderer, settingsOpen]);
|
|
||||||
|
|
||||||
const status = runtimePresentation(runtimeState);
|
const status = runtimePresentation(runtimeState);
|
||||||
const viewBlocking = !viewInitialized && controller.state !== "ready";
|
const viewBlocking = !viewInitialized && controller.state !== "ready";
|
||||||
const runtimeBlocking = runtimeState.phase === "gateway-unavailable"
|
const runtimeBlocking = runtimeState.phase === "gateway-unavailable"
|
||||||
@@ -234,15 +192,30 @@ export function WorldMapWorkspace({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Toolbar<ToolbarAction>
|
<div className="mission-map__map-actions" aria-label="Управление картой">
|
||||||
className="mission-map__toolbar"
|
<IconButton
|
||||||
placement="bottom"
|
className="mission-map__map-action"
|
||||||
label="Инструменты карты"
|
label="Настройки карты"
|
||||||
minSize={42}
|
aria-pressed={settingsOpen}
|
||||||
maxSize={54}
|
onClick={() => {
|
||||||
lensCount={3}
|
setSettingsOpen((open) => !open);
|
||||||
items={toolbarItems}
|
setLayersOpen(false);
|
||||||
/>
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="settings" size={18} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
className="mission-map__map-action"
|
||||||
|
label="Слои карты"
|
||||||
|
aria-pressed={layersOpen}
|
||||||
|
onClick={() => {
|
||||||
|
setLayersOpen((open) => !open);
|
||||||
|
setSettingsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="grid" size={18} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
{layersOpen ? (
|
{layersOpen ? (
|
||||||
<MapGlassSurface
|
<MapGlassSurface
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
import { after, before, test } from "node:test";
|
import { after, before, test } from "node:test";
|
||||||
|
|
||||||
import { createServer } from "vite";
|
import { createServer } from "vite";
|
||||||
@@ -105,3 +106,16 @@ test("map view fails closed on credentials, runtime URLs and unbound selection",
|
|||||||
/stable subject id|стабильный subject id/i,
|
/stable subject id|стабильный subject id/i,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("map workspace keeps the canonical top actions without a bottom toolbar", async () => {
|
||||||
|
const source = await readFile(
|
||||||
|
new URL("../src/workspaces/map/WorldMapWorkspace.tsx", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.doesNotMatch(source, /<Toolbar/);
|
||||||
|
assert.match(source, /className="mission-map__map-actions"/);
|
||||||
|
assert.match(source, /label="Настройки карты"/);
|
||||||
|
assert.match(source, /label="Слои карты"/);
|
||||||
|
assert.doesNotMatch(source, /label="Сбросить камеру"/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user