feat(map): add reference layers and stabilize workspace controls

This commit is contained in:
Codex
2026-07-25 13:12:24 +03:00
parent c859ae4db0
commit eaecce9f56
29 changed files with 1593 additions and 247 deletions
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
test("Cesium elevated targets keep their profile border while labels occlude them inside the scene overlay", async () => {
const [renderer, cesiumDisplay] = await Promise.all([
readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"),
readFile(new URL("../node_modules/@cesium/engine/Source/DataSources/DataSourceDisplay.js", import.meta.url), "utf8"),
]);
assert.match(renderer, /BillboardGraphics/);
assert.match(renderer, /elevatedTargetImage\(\s*color,\s*Color\.fromCssColorString\(target\.outlineColor\)\.withAlpha\(target\.outlineOpacity\),\s*target\.outlineWidthPx,\s*target\.headSizePx/);
assert.match(renderer, /entity\.point = undefined;\s*entity\.billboard = new BillboardGraphics/);
assert.match(renderer, /disableDepthTestDistance: Number\.POSITIVE_INFINITY/);
assert.match(renderer, /outlineWidth: 0,\s*style: LabelStyle\.FILL/);
assert.doesNotMatch(renderer, /style: 2/);
assert.ok(cesiumDisplay.indexOf("new BillboardVisualizer") < cesiumDisplay.indexOf("new LabelVisualizer"));
});
test("Map Inspector open section is controlled and included in the saved page layout", async () => {
const [inspector, preview, server, mcpSchema, manifestSchema] = await Promise.all([
readFile(new URL("../packages/ui-react/src/Inspector.tsx", import.meta.url), "utf8"),
readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"),
readFile(new URL("../server/catalog-server.mjs", import.meta.url), "utf8"),
readFile(new URL("../server/foundry-mcp.mjs", import.meta.url), "utf8"),
readFile(new URL("../registry/schemas/application-manifest-v0.1.schema.json", import.meta.url), "utf8"),
]);
assert.match(inspector, /openSections\?: string\[\]/);
assert.match(inspector, /onOpenSectionsChange\?: \(ids: string\[\]\) => void/);
assert.match(inspector, /onOpenSectionsChange\?\.\(\[\.\.\.next\]\)/);
assert.match(preview, /const \[inspectorOpenSections, setInspectorOpenSections\]/);
assert.match(preview, /openSections=\{inspectorOpenSections\}/);
assert.match(preview, /onOpenSectionsChange=\{setInspectorOpenSections\}/);
assert.match(preview, /referenceLayers,\s*inspectorOpenSections,\s*subjectStates/);
assert.match(server, /validateMapInspectorOpenSections/);
assert.match(server, /inspectorOpenSections: \["map-base"\]/);
assert.match(mcpSchema, /inspectorOpenSections:[\s\S]*?maxItems: 1/);
assert.match(manifestSchema, /"inspectorOpenSections"[\s\S]*?"maxItems": 1/);
});
test("canonical station profiles retain visible target borders while label text stays halo-free", async () => {
const registry = JSON.parse(await readFile(
new URL("../registry/map-presentation-profiles.json", import.meta.url),
"utf8",
));
for (const profile of registry.profiles) {
assert.equal(profile.label.outlineWidthPx, 0);
}
const stationProfiles = registry.profiles.filter((profile) => profile.id.startsWith("map.reference.transport."));
assert.equal(stationProfiles.length, 3);
stationProfiles.forEach((profile) => assert.ok(profile.target.outlineWidthPx > 0));
const zone = registry.profiles.find((profile) => profile.target.variant === "surface-fill");
assert.ok(zone.target.outlineWidthPx > 0);
});
+54 -5
View File
@@ -2,22 +2,22 @@ import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
test("Objects menu treats each binding as a persisted visibility layer", async () => {
test("Objects menu opens controllable groups from the row and keeps plain bindings as visibility layers", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
assert.match(preview, /const toggleSubjectVisibility = \(bindingId: string\)/);
assert.match(preview, /role="menuitemcheckbox"/);
assert.match(preview, /aria-checked=\{visible\}/);
assert.match(preview, /onClick=\{\(\) => toggleSubjectVisibility\(summary\.bindingId\)\}/);
assert.match(preview, /role=\{hasControls \? "menuitem" : "menuitemcheckbox"\}/);
assert.match(preview, /if \(hasControls\) \{\s*openSubjectWindow\(summary\.bindingId\);\s*close\(\)/);
assert.match(preview, /toggleSubjectVisibility\(summary\.bindingId\)/);
assert.match(preview, /visible: !state\.visible/);
assert.match(preview, /слой скрыт · \$\{summary\.total\} объектов/);
assert.doesNotMatch(preview, /Фильтры и счётчики: \$\{summary\.displayName\}/);
});
test("a subject window exists only when the presentation profile exposes controls", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
assert.match(preview, /function hasSubjectWindowControls\(profile: MapPresentationProfile\)/);
assert.match(preview, /\{hasControls \? \(/);
assert.match(preview, /if \(!state\?\.window\.open \|\| !hasSubjectWindowControls\(summary\.profile\)\) return null/);
assert.match(preview, /window: \{ \.\.\.state\.window, open: false \}/);
});
@@ -28,3 +28,52 @@ test("hidden and visible layers have an explicit visual state", async () => {
assert.match(styles, /\.catalog-map-fixture__objects-menu-item\[data-visible\]/);
assert.match(styles, /\.catalog-map-fixture__objects-menu-item:not\(\[data-visible\]\)/);
});
test("joined detail aspects do not become independent map layers", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
assert.match(preview, /dataProductBindings\.filter\(\(binding\) => !binding\.joinToBindingId\)/);
assert.match(preview, /runtimeBindings\.filter\(\(binding\) => primaryBindingIds\.has\(binding\.bindingId\)\)/);
assert.match(preview, /runtimeBindings=\{\[\.\.\.primaryRuntimeBindings, \.\.\.referenceRuntimeBindings\]\}/);
});
test("reference stations are first-class Objects menu layers without provider settings actions", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
assert.match(preview, /const referenceObjectSummaries = useMemo/);
assert.match(preview, /const objectLayerCount = presentationSummaries\.length \+ referenceObjectSummaries\.length/);
assert.match(preview, /referenceObjectSummaries\.map\(\(\{ layer, displayName, total \}\)/);
assert.match(preview, /candidate\.id === layer\.id \? \{ \.\.\.candidate, visible: !candidate\.visible \} : candidate/);
assert.match(preview, /role="menuitemcheckbox"/);
});
test("facet window auto-fits expanded and collapsed content inside map workspace", async () => {
const [preview, workspace] = await Promise.all([
readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"),
readFile(new URL("../packages/ui-react/src/WorkspaceWindow.tsx", import.meta.url), "utf8"),
]);
assert.match(preview, /minHeight=\{220\}\s*autoHeight/);
assert.match(workspace, /autoHeight\?: boolean/);
assert.match(workspace, /content\.scrollHeight/);
assert.match(workspace, /availableHeight = Math\.max\(0, nextBounds\.height - normalized\.y\)/);
assert.match(workspace, /new ResizeObserver\(scheduleFit\)/);
});
test("facet tree selection focuses the subject without resetting a compatible detail tab", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
const renderer = await readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8");
assert.match(preview, /const \[expandedFacetRows, setExpandedFacetRows\]/);
assert.match(preview, /const handleSelectAndFocus = useCallback/);
assert.match(preview, /mapRendererRef\.current\?\.focusRuntimeEntity\(entityId\)/);
assert.match(preview, /setSubjectCardTabId\(\(current\) =>/);
assert.match(preview, /profile\?\.tabs\.some\(\(tab\) => tab\.id === current\)/);
assert.match(preview, /\(profile\?\.defaultTabId \?\? "overview"\)/);
assert.match(renderer, /camera\.pickEllipsoid/);
assert.match(renderer, /Cartesian3\.subtract\(camera\.position, viewportCenter/);
assert.match(renderer, /heading: camera\.heading/);
assert.match(renderer, /pitch: camera\.pitch/);
assert.match(renderer, /roll: camera\.roll/);
assert.match(renderer, /duration: 2\.1/);
});
+40
View File
@@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const root = new URL("../", import.meta.url);
test("Map Page registers a provider-neutral reference-points slot and three station profiles", async () => {
const pages = JSON.parse(await readFile(new URL("registry/pages.json", root), "utf8"));
const profiles = JSON.parse(await readFile(new URL("registry/map-presentation-profiles.json", root), "utf8"));
const mapPage = pages.templates.find((template) => template.id === "map");
const stationProfiles = profiles.profiles.filter((profile) => profile.id.startsWith("map.reference.transport."));
assert.ok(mapPage.slots.some((slot) => slot.id === "reference-points" && slot.kind === "entity-stream"));
assert.deepEqual(
stationProfiles.map((profile) => profile.id).sort(),
[
"map.reference.transport.metro.v1",
"map.reference.transport.railway-station.v1",
"map.reference.transport.terminal.v1",
],
);
assert.deepEqual(
new Set(stationProfiles.flatMap((profile) => profile.semanticTypes)),
new Set(["map.station", "map.terminal"]),
);
assert.ok(stationProfiles.every((profile) => profile.target.variant === "elevated-spike"));
});
test("existing layouts upgrade reference layers on read and renderer accepts their slot", async () => {
const server = await readFile(new URL("server/catalog-server.mjs", root), "utf8");
const renderer = await readFile(new URL("apps/catalog/src/CesiumMapRenderer.tsx", root), "utf8");
const runtime = await readFile(new URL("apps/catalog/src/useMapReferenceRuntime.ts", root), "utf8");
assert.match(server, /pageLayoutMatch\[1\] === "map" \? validateMapPageLayout\(stored\) : stored/);
assert.match(server, /referenceLayers: structuredClone\(canonicalMapReferenceLayers\)/);
assert.match(renderer, /binding\.slotId === "reference-points"/);
assert.match(runtime, /\/api\/map-gateway\/api\/map\/reference-sources\/v1\/profiles\//);
assert.match(runtime, /allowedAttributes = new Set\(\["name", "category", "network", "operator", "official_name", "local_name", "uic_ref", "wheelchair"\]\)/);
assert.doesNotMatch(runtime, /credential|accessToken|providerEndpoint|rawPayload/);
});
+50
View File
@@ -0,0 +1,50 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { RangeControl } from "../packages/ui-react/dist/index.js";
test("RangeControl keeps native drag semantics and exposes one persistent exact-value editor", () => {
const markup = renderToStaticMarkup(createElement(RangeControl, {
label: "Размер головки",
value: 20,
min: 1,
max: 32,
step: 1,
formatValue: (value) => `${value} px`,
onChange: () => {},
}));
assert.match(markup, /type="range"/);
assert.match(markup, /aria-valuetext="20 px"/);
assert.match(markup, /class="nodedc-range__editor"/);
assert.match(markup, /aria-label="Размер головки: точное значение"/);
assert.doesNotMatch(markup, /nodedc-range__value-hit/);
});
test("exact editing is a stable native caret field without remount, forced selection or a dark editor box", async () => {
const [component, styles, windowComponent] = await Promise.all([
readFile(new URL("../packages/ui-react/src/RangeControl.tsx", import.meta.url), "utf8"),
readFile(new URL("../packages/ui-core/styles.css", import.meta.url), "utf8"),
readFile(new URL("../packages/ui-react/src/Window.tsx", import.meta.url), "utf8"),
]);
assert.match(component, /normalizeEditedRangeValue\(parsed, min, max, step\)/);
assert.match(component, /event\.key === "Enter"/);
assert.match(component, /event\.key === "Escape"/);
assert.match(component, /event\.stopPropagation\(\)/);
assert.match(component, /cancelNextBlurRef/);
assert.match(component, /event\.currentTarget\.blur\(\)/);
assert.match(component, /onBlur=\{\(\) => \{/);
assert.doesNotMatch(component, /\.select\(\)/);
assert.doesNotMatch(component, /setSelectionRange/);
assert.doesNotMatch(component, /\{editing \? \(\s*<input/);
assert.match(styles, /\.nodedc-range input\[type="range"\][\s\S]*?z-index: 4/);
assert.match(styles, /\.nodedc-range__editor[\s\S]*?z-index: 5[\s\S]*?background: transparent[\s\S]*?color: transparent[\s\S]*?caret-color: transparent[\s\S]*?box-shadow: none/);
assert.match(styles, /\.nodedc-range__editor\[data-active\][\s\S]*?caret-color: currentColor/);
assert.match(windowComponent, /const onCloseRef = useRef\(onClose\)/);
assert.match(windowComponent, /onCloseRef\.current = onClose/);
assert.match(windowComponent, /onCloseRef\.current\(\)/);
assert.doesNotMatch(windowComponent, /\[closeOnEscape, onClose, open, shouldLockBodyScroll, shouldTrapFocus\]/);
});