Level: organism. Inventory status: local-code.
Purpose
Provide a reusable desktop geographic map that resizes with its container and supports pan, zoom, rotation, light/dark cartographic styles, markers and polygon editing. It owns client-side map interaction only; feature data loading, permissions, persistence and navigation remain outside the design system.
Confirmed correction target
The resumed M-003 correction implements and verifies these confirmed requirements:
- 20 px control insets;
- real light and dark cartographic Storybook underlays supplied only by a demo provider, while the runtime remains provider-neutral;
- guaranteed visibility of all supplied geometry after every successful
style.load; - one surfaced Draw action bar made from Flat Primary actions, with Delete-all adjacent to the Delete tool;
- polygon, zoom and auxiliary Tooltip placement at right, left and top respectively;
- a cursor-following draw hint until the first accepted polygon point;
- an edit-selection hint, persistent edit mode, pointer cursor on vertices and a dirty-state Save action beside Edit;
- staged delete selection with a danger highlight, explicit Delete/Delete-all actions and owned Modal confirmation before geometry changes;
- one-level undo for the last confirmed single or Delete-all operation;
- fixed polygon-tool rows that do not move when contextual actions appear;
- light Tooltip surfaces with dark text for every control Tooltip and cursor-following hint in the dark map theme;
- polygon-to-polygon snapping during creation and vertex editing, using a 16 px pointer threshold for both vertices and line segments;
- resize recovery that coalesces host observations, skips zero-size frames and repaints the existing MapLibre map without replacing its style;
- circular Number/Icon pins and separate Pointer anatomy defined by Map Pin.
Runtime boundary
M-003 uses MapLibre GL JS 6.8.0 for rendering and interaction, Terra Draw 1.32.3 with its MapLibre adapter for polygon creation/editing, and caller-provided light/dark style definitions. Figma raster backgrounds are visual evidence only and must never be shipped as the interactive map.
Runtime code stays provider-neutral: it accepts MapLibre styles and contains no owned tile-provider endpoint or credential. Storybook must demonstrate both themes with real cartographic light/dark styles from an explicitly demo-only provider; a flat color layer is not an acceptable dark-map story.
The Angular build publishes MapLibre's worker and shared ESM modules under the
application base URL. The component resolves the worker from document.baseURI
before creating a map, so vector tiles and owned GeoJSON layers work in both the
application and Storybook builds.
Anatomy and layout
- A MapLibre canvas fills the host's current inline and block size.
mainhas no component-owned border or radius.miniuses a secondary 1 px stroke and the large clipping radius.- Map Zoom is vertically centered at the right edge with a 20 px inset.
- Info, Settings and fullscreen actions form an 8 px-gap group with 20 px bottom/right insets.
- Polygon actions form a vertical group with 20 px top/left insets.
- Draw mode exposes one action-bar surface to the right of the Draw tool. The surface uses the level-1-base background, secondary 1 px stroke, 8 px radius, dropdown shadow, 12 px inline/4 px block padding and 12 px gap. Its Finish, Undo-last-point and Cancel actions reuse Flat Primary at 24 px height.
- The Delete row uses the same surfaced action treatment. It exposes separate
Flat Danger actions for the selected polygon (
Удалить) and all polygons (Удалить все), plusОтменить удалениеwhile a one-level undo is available. Delete actions open an owned Modal confirmation instead of mutating geometry directly. - Single-delete and Delete-all confirmations are distinct owned Modal dialogs.
Their destructive actions repeat the initiating label;
Отменаis the safe initial focus target. - Each Draw/Edit/Delete tool owns a fixed row. Contextual action surfaces are rendered beside their owning tool and never change the vertical positions of another polygon row. Opening Draw actions therefore must not push Edit or Delete down.
- Pins render above the cartographic style but below the controls; provider attribution stays available inside the MapLibre canvas.
The broad Figma sentence that “all controls” are bottom-right is interpreted as the auxiliary Info/Settings/fullscreen group: exact component layout and the same specification place zoom right-center and polygon actions top-left.
Variants and controls
main: Zoom and polygon controls are available by default. Info and Settings appear only when their matching template slot is provided.mini: Zoom and Enter fullscreen are available by default; polygon controls are omitted. Info and Settings still require their matching slots.controlsmay hide a group without changing its location or visual owner.- Enter/Exit fullscreen uses the browser Fullscreen API on the mini-map host and
emits
fullscreenChange; it does not invent a route or modal. - Info and Settings toggle anchored dialog overlays whose contents are supplied through feature-owned template slots. Neither panel body is defined by the provided Figma nodes.
Map interaction
- Pointer drag and touch pan move the viewport.
- Wheel/trackpad, pinch and Map Zoom change zoom within configured bounds.
- Mouse modifier/right-button drag, touch rotation and MapLibre keyboard controls rotate the map. Bearing is part of the controlled viewport.
- A
ResizeObserverwatches the Map host because container resizing may happen without a window resize. Notifications are coalesced into one pendingrequestAnimationFrame. A frame with zero inline or block size is skipped; the observer remains active so the next non-zero frame can recover the map. For a valid size, call MapLibreresize(), thentriggerRepaint(), then update pin positions. Destroy cancels any queued animation frame. Resize does not callsetStyle(), recreate MapLibre or recreate Terra Draw. - Theme changes replace the base style while preserving viewport, markers, polygon data and enabled interaction state. Owned sources/layers are restored after the new style loads.
- The rendered cartographic theme also selects Map-owned Tooltip contrast. Light maps use the default dark Tooltip surface with light text. Dark maps invert all control overlays and cursor-following hints to a light surface with dark text.
- After every successful
style.load, every supplied Polygon, MultiPolygon, LineString and MultiLineString is visible in view mode. Terra Draw may own the currently editable geometry, but must not be the only visibility path for ordinary caller-supplied single geometries. - Initialization/style/fullscreen failures emit
loadError; the component remains labelled and does not expose credentials or provider responses.
Polygon behavior
viewallows map navigation without editing geometry.draw-polygonstarts Terra Draw polygon creation and exposes finish, undo-last and cancel actions.- During
draw-polygon, every new coordinate may snap to the nearest vertex or line segment of another Polygon feature when the pointer is within 16 screen pixels. LineString features and the Polygon currently being drawn are not snapping targets. - Before the first accepted vertex, Draw mode shows a non-interactive hint next to the cursor: “Нажмите, чтобы начать рисовать полигон”. It follows pointer movement over the canvas and closes on the first accepted vertex, pointer leave, cancel, finish or any mode change. It does not enter the focus order or intercept map input.
edit-polygonallows selection and vertex editing of an existing polygon.- Until a polygon is selected in
edit-polygon, a non-interactive cursor-following hint reads: “Нажмите на полигон, который хотите отредактировать”. The hint closes after polygon selection, on pointer leave or when the mode ends. - Selecting a polygon or changing its geometry must not automatically return to
view; edit mode stays active until explicit completion. - Hovering an editable vertex uses the pointer cursor so the hit target is discoverable. Other map areas retain the appropriate map/edit cursor.
- Dragging an individual vertex in
edit-polygonmay snap it to the nearest vertex or line segment of another Polygon feature within the same 16 px pointer threshold. Lines, the selected Polygon itself and whole-feature drag are excluded from snapping. - Snapping copies the accepted coordinate only. Polygons remain separate GeoJSON features: the component does not merge them, create a shared-boundary model or keep previously joined coordinates synchronized after a later edit.
- After the first geometry change in the current edit session, a “Сохранить”
Flat Primary action appears directly beside the Edit tool without moving any
polygon row. Activating Save explicitly completes the editing session,
clears its dirty state and returns to
view. Save describes UI completion, not backend persistence;geometryremains the controlled data boundary. delete-polygonis a staged destructive workflow. Clicking a Polygon or MultiPolygon selects it throughselectedGeometryIdand applies a semantic danger fill/outline; it does not changegeometry. Clicking another polygon moves the pending selection and highlight to that feature.- A selected polygon exposes a separate
Удалитьaction. Activating it opens the owned single-delete Modal; only its confirm action removes the selected feature.Удалить всеopens a distinct owned Modal and only its confirm action removes every Polygon and MultiPolygon while preserving LineString and MultiLineString features. Cancelling either Modal leavesgeometryunchanged. delete-polygonremains active after a confirmed single-delete, confirmed Delete-all or undo. Confirmed deletion clears the pending selection and returns the cursor hint to its unselected delete-mode behavior.- The last confirmed single-delete or Delete-all operation creates one undo
record containing the removed features and their original collection
positions.
Отменить удалениеremains available after the operation and restores exact IDs, properties, geometry and feature order. A later confirmed deletion replaces the previous undo record; this is not a multi-step history. - Any external
geometryupdate or non-delete geometry mutation invalidates both the undo record and a pending delete selection. Stale features must not be reintroduced over caller data, and a removed/replaced selected ID must not remain highlighted or confirmable. - While
delete-polygonis active over the canvas, a non-interactive cursor-following hint reads: “Нажмите на полигон, который хотите удалить”. It closes after a polygon is selected, on pointer leave or when the mode ends. - Geometry is controlled as a GeoJSON FeatureCollection of Polygon, MultiPolygon, LineString and MultiLineString features and emits after changes. Backend persistence is out of scope.
- Clicking a polygon vertex in edit mode selects it for keyboard editing. Arrow keys move the selected vertex by 1 screen pixel; Shift+Arrow moves it by 10 screen pixels. The component announces vertex selection and movement. Consumers with precision-coordinate workflows should still provide an equivalent coordinate/list editor alongside the spatial canvas.
Markers
Pins are caller-provided records with stable ID, coordinates, accessible label
and typed Map Pin content. Activating one updates selectedPinId. The component
does not fetch marker content or open a product-specific details card.
Accessibility
- The MapLibre interaction surface is a labelled application region.
ariaLabelshould identify its purpose in the current screen, not merely say “карта”. - Keep the MapLibre keyboard surface focusable and visibly focused. Keyboard pan, zoom and rotation must remain enabled.
- Every visible control is a labelled native button with Tooltip support on hover and focus; visual glyphs remain decorative.
- Polygon-tool Tooltips prefer
right, Map Zoom Tooltips preferleft, and bottom-right auxiliary Tooltips prefertop; existing Tooltip fallbacks may move them only when the preferred side cannot fit the viewport. - Dark-map control Tooltips and non-interactive cursor hints use the inverted Tooltip appearance so their surfaces remain distinct from the cartography.
- Danger color is not the only delete-selection cue: the selected polygon also
has a distinct outline, and an
aria-liveannouncement reports selection, confirmed deletion, Delete-all and successful undo. - Single-delete and Delete-all use the owned Modal accessibility contract. A
safe non-destructive close control receives initial focus, Escape is
equivalent to cancel, and cancel returns focus to the action that opened the
Modal. After confirmation, focus
moves to the available
Отменить удалениеaction so undo remains keyboard reachable even when the initiating action disappears. - Delete and undo controls are labelled native buttons. Opening or cancelling a
confirmation does not silently change
geometryor delete-mode state. - Provider attribution and required license links must remain available.
- Interactive marker labels must expose location meaning. Dense visual pins need an equivalent accessible list when spatial targeting is insufficient.
- Motion must respect
prefers-reduced-motion; camera transitions become immediate while direct manipulation remains available.
Token and theme use
- Mini frame:
--color-stroke-secondary,--stroke-100,--radius-l. - Control spacing:
--space-200for auxiliary gaps and--space-500for the confirmed 20 px edge inset. The Draw action bar uses--space-300and--space-100for its verified internal spacing. - Control visuals and pins use their owning component contracts.
themeselects the cartographic light/dark style; it does not introduce component-local raw replacements for the global semantic UI tokens.- In the light theme, control Tooltips and cursor hints use
--color-background-tooltipwith--color-text-icon-primary-inverted. In the dark theme they use--color-background-level-1-basewith--color-text-icon-primary. - Pending delete selection uses the existing semantic danger color for its fill/outline treatment; do not add a raw red or a Map-only danger token. The confirmation surface and actions use the owned Modal and Button contracts.
- The six Figma background examples (
world,russia,moscow-region,moscow-large,moscow-medium,moscow-details) are continuous-zoom visual references, not public runtime enum values.
Storybook coverage target
The main story must show a real cartographic underlay, at least one visible polygon, one visible line and circular Number/Icon pins. Pointer has a separate Map Pin story because it is a distinct marker anatomy. Light and Dark stories must both retain streets, labels and geographic context. Dark demonstrates the inverted control Tooltip and cursor hint. Polygon Editing Snapping demonstrates two separate polygons and the 16 px vertex/segment snapping behavior. The story may name its provider as demo-only; that provider must not become a runtime default.
Polygon Deletion Safety demonstrates Polygon and MultiPolygon selection, danger highlighting, separate single/Delete-all confirmations, cancellation, one-level undo with exact restoration and persistent delete mode. Resize Recovery exercises rapid host resizing and zero-size-to-visible recovery in both themes; streets, labels, polygons and pins remain visible without a style reload.
Focused regression target
- The Draw/Edit/Delete row positions are identical before and after contextual actions appear.
- Delete-all is adjacent to Delete and uses Flat Danger.
- Edit hint remains visible until polygon selection; selecting and changing a
polygon keep
mode === 'edit-polygon'. - Save is absent before a geometry change, appears beside Edit after the first change and explicitly completes the editing session.
- Editable vertex hover uses the pointer cursor.
- Delete mode exposes the confirmed delete-selection hint.
- Clicking a polygon in delete mode changes only pending selection and danger
highlight;
geometryis unchanged until the separate action and Modal confirm. - Single-delete and Delete-all have distinct confirmations. Cancel and Escape
preserve geometry, while confirm removes only the intended polygon scope,
preserves line features and keeps
mode === 'delete-polygon'. - Undo after single-delete and Delete-all restores exact feature IDs, properties, geometry and collection order. Only the latest confirmed delete is undoable, and undo remains keyboard reachable after deletion.
- An external or non-delete geometry mutation clears pending delete selection and invalidates undo without restoring stale features.
- Modal focus starts on a safe non-destructive close control, returns to the
initiating action on cancel and moves to
Отменить удалениеafter confirmation. Live announcements cover selection, deletion and undo; danger color is not the sole selection cue. - Multiple ResizeObserver notifications before paint schedule one resize frame.
Zero-size frames do not call MapLibre; the next valid frame calls
resize(),triggerRepaint()and pin update in that order, withoutsetStyle()or draw reinitialization. Destroy cancels the queued frame. - Every control Tooltip and cursor hint uses a light surface with dark text when
the rendered map theme is
dark, and retains the default contrast when it islight. - Polygon creation snaps only to vertices and line segments of another Polygon within 16 px; line geometry and the in-progress Polygon are excluded.
- Dragging a selected Polygon vertex uses the same snapping targets and threshold, while the two Polygon feature IDs and independent geometries are preserved.
- Moving a joined coordinate later does not mutate the neighboring Polygon; snapping is not a persistent topological relationship.
Local component contract
This is the M-003 owned contract for component.map.
Public types
import type {
Feature,
FeatureCollection,
LineString,
MultiLineString,
MultiPolygon,
Polygon,
} from 'geojson';
import type { StyleSpecification } from 'maplibre-gl';
export type WblMapVariant = 'main' | 'mini';
export type WblMapTheme = 'light' | 'dark';
export type WblMapMode = 'view' | 'draw-polygon' | 'edit-polygon' | 'delete-polygon';
export type WblMapLineStyle = 'solid' | 'dashed';
export type WblMapStyle = string | StyleSpecification;
export interface WblMapStyles {
readonly light: WblMapStyle;
readonly dark: WblMapStyle;
}
export interface WblMapViewport {
readonly center: readonly [longitude: number, latitude: number];
readonly zoom: number;
readonly bearing: number;
}
export interface WblMapControls {
readonly zoom?: boolean;
readonly info?: boolean;
readonly settings?: boolean;
readonly fullscreen?: boolean;
readonly polygons?: boolean;
}
export interface WblMapFeatureProperties {
readonly strokeColor?: `#${string}`;
readonly fillColor?: `#${string}`;
readonly fillOpacity?: number;
readonly lineStyle?: WblMapLineStyle;
readonly [key: string]: unknown;
}
export type WblMapGeometry = Polygon | MultiPolygon | LineString | MultiLineString;
export type WblMapFeature = Feature<WblMapGeometry, WblMapFeatureProperties> & {
readonly id: string | number;
};
export type WblMapFeatureCollection = FeatureCollection<WblMapGeometry, WblMapFeatureProperties> & {
readonly features: WblMapFeature[];
};
export interface WblMapLoadError {
readonly code: 'missing-style' | 'map-load' | 'fullscreen';
readonly message: string;
readonly cause?: unknown;
}
export interface WblMapOptions {
readonly styles: WblMapStyles | null;
}
WblMapPinConfig and its discriminated content types are owned by Map Pin.
Feature properties are pass-through presentation metadata; no backend fields
are required by the design-system type.
wblMapOptionsProvider(options) may provide application-level default styles
through WBL_MAP_OPTIONS; the component-level styles input takes precedence.
Class and selector
WblMapComponent—wbl-map.
Inputs
variant = input<WblMapVariant>('main')theme = input<WblMapTheme>('light')styles = input<WblMapStyles | null>(null);nullresolves throughWBL_MAP_OPTIONScontrols = input<WblMapControls>(DEFAULT_CONTROLS)pins = input<readonly WblMapPinConfig[]>([])ariaLabel = input('Географическая карта')
Tooltip appearance and the 16 px polygon-snapping threshold are owned runtime
rules derived from theme and the active polygon mode; they do not add Map
inputs.
Models
viewport = model<WblMapViewport>(DEFAULT_VIEWPORT)geometry = model<WblMapFeatureCollection>(WBL_EMPTY_MAP_GEOMETRY)mode = model<WblMapMode>('view')selectedPinId = model<string | null>(null)selectedGeometryId = model<string | null>(null)
selectedGeometryId owns the active Polygon/MultiPolygon selection in both
edit and delete modes. In delete mode selection is pending only and cannot
mutate geometry until the matching owned Modal is confirmed.
Outputs
mapReady = output<void>()loadError = output<WblMapLoadError>()fullscreenChange = output<boolean>()
Delete confirmation, one-level undo and resize scheduling are internal Map
behavior. They add no public input, model or output. Confirmed delete and undo
emit only through the existing controlled geometry model; backend persistence
and rollback remain consumer responsibilities.
Slots
ng-template[wblMapInfo]— contents of the anchored Info dialog.ng-template[wblMapSettings]— contents of the anchored Settings dialog.ng-template[wblMapLoading]— optional loading-state content.ng-template[wblMapError]— optional error-state content withWblMapErrorContext.
Feature-owned detail cards stay outside this reusable owner.
Local evidence target
- Code:
src/app/design-system/organisms/map/map.component.ts. - Public models:
src/app/design-system/organisms/map/map.models.tswhen the implementation separates them from the component. - Focused test:
src/app/design-system/organisms/map/map.component.spec.ts. - Public export:
src/app/design-system/index.ts. - Storybook:
storybook/stories/map.stories.ts.
Figma evidence
- Map component, node
10064:27554: main/mini variants, control visibility and layout. - Map specification, node
5833:13685: size usage, zoom interaction, control intent, polygons, pins and lines. - Light/dark background examples, node
10054:31330: six scale references for each theme. - Dark map composition, node
23125:654: real detailed dark cartography and control placement. - Draw action bar, node
14577:5541: one surfaced action group with Flat Primary actions. - Cursor-hint Tooltip, node
14577:5540: verified hint copy and visual treatment. - Pointer correction, node
18205:808: separate Pointer geometry and shadow.
Exact-node metadata, design context, variables and screenshots were read on 2026-09-10. Figma establishes visual intent, not the MapLibre API. Published component keys were unavailable, so no assembly sidecar is claimed yet.
Documentation task
M-003 · GitLab issue ·
actor artem-mokin / GitLab 26942597 · agent Specs subagent ·
run/session 7a124ab4-0ecb-435c-9cb5-dd9df24c995e.