wbDesign Wiki
Storybook
Angularorganism
Для этого компонента пока нет примера в опубликованном Storybook.

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.
  • main has no component-owned border or radius.
  • mini uses 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.
  • controls may 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 ResizeObserver watches the Map host because container resizing may happen without a window resize. Notifications are coalesced into one pending requestAnimationFrame. 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 MapLibre resize(), then triggerRepaint(), then update pin positions. Destroy cancels any queued animation frame. Resize does not call setStyle(), 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

  • view allows map navigation without editing geometry.
  • draw-polygon starts 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-polygon allows 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-polygon may 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; geometry remains the controlled data boundary.
  • delete-polygon is a staged destructive workflow. Clicking a Polygon or MultiPolygon selects it through selectedGeometryId and applies a semantic danger fill/outline; it does not change geometry. 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 leaves geometry unchanged.
  • delete-polygon remains 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 geometry update 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-polygon is 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. ariaLabel should 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 prefer left, and bottom-right auxiliary Tooltips prefer top; 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-live announcement 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 geometry or 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-200 for auxiliary gaps and --space-500 for the confirmed 20 px edge inset. The Draw action bar uses --space-300 and --space-100 for its verified internal spacing.
  • Control visuals and pins use their owning component contracts.
  • theme selects 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-tooltip with --color-text-icon-primary-inverted. In the dark theme they use --color-background-level-1-base with --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; geometry is 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, without setStyle() 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 is light.
  • 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); null resolves through WBL_MAP_OPTIONS
  • controls = 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 with WblMapErrorContext.

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.ts when 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

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.

WB Logistics · Design SystemСборка 91da5ae

Поиск по Wiki

Начните вводить название или текст