Booking Client (Renderer) API Changes - v1.70.0

Release: v1.70.0 Package: @seatmap.pro/renderer Date: 2026-08-02

MetaTitle: Renderer API Changes 1.70.0 - Seatmap.pro

MetaDescription: New renderer APIs in 1.70.0: admin flat section view, seat block selection, configurable hotkeys, per-source outline styles and branding settings.


Summary

Backward Compatible: YES New Features: 5 Deprecations: 0 Bug Fixes: 3 Architecture Improvements: Keyboard input in the admin renderer moved behind a hotkey controller with configurable bindings.

Migration Required: NO - every new API is additive and optional. One behavioural change is worth reviewing: a numeric price reported to your callbacks now preserves decimals instead of truncating them.


New Features

Flat section view (SEAT-1099)

Status: New

Description: Flattens a single section onto its seating grid in the admin renderer, matching what the schema editor shows. The rest of the map is dimmed behind a backdrop, and row and seat labels are drawn over the flattened grid.

API:

interface IAdminRenderer extends IRenderer {
  setFlatSectionView: (sectionId: number | null) => Promise<boolean>;
}

interface IAdminRendererSettings {
  onFlatSectionViewChange?: (sectionId: number | null) => void;
}

Example:

const renderer = new SeatmapAdminRenderer(document.getElementById('renderer-container'), {
  publicKey: 'your-public-key',
  onFlatSectionViewChange: (sectionId) => {
    console.log(sectionId === null ? 'venue view' : `flat: ${sectionId}`);
  },
});
await renderer.loadEvent('your-event-id');

const flattened = await renderer.setFlatSectionView(sectionId);

await renderer.setFlatSectionView(null);

Parameters:

Parameter Type Required Default Description
sectionId number | null Yes - Section to flatten, or null to exit the view

Returns: Promise<boolean> - true when the view is flat for that section, or when null was passed; false when the section cannot be flattened.

Behavior:

  • A section that is general admission, a table layout, an unknown id, or has no grid resolves false and leaves the view unchanged.
  • Requesting the section that is already flat resolves true and does nothing. The call never toggles the view off.
  • Exiting always resolves true, whether or not anything was flat.
  • onFlatSectionViewChange fires on every entry and exit, including the forced exit on renderer teardown or a schema reload.
  • onFlatSectionViewChange must be supplied at construction time. The renderer closes over the constructor settings object and Context keeps a shallow copy, so assigning the callback afterwards has no effect.

Performance Impact: LOW - the flattened grid is built once per entry.

Related Files:

  • products/booking-client/src/admin-renderer/flatten/SectionFlattenController.ts - NEW: entry, exit and teardown handling
  • products/booking-client/src/admin-renderer/flatten/RowSeatLabelOverlay.ts - NEW: row and seat labels over the flattened grid

Seat block selection (SEAT-1048)

Status: New

Description: Selects the rectangular block of seats spanning the section-local grid coordinates between an anchor seat and a focus seat. It works on the section’s own row and seat grid rather than on screen geometry, so the block follows the seating layout even when the section is curved or rotated.

API:

selectSeatBlock(
  anchorSeatId: number,
  focusSeatId: number,
  mode?: RendererSelectMode,
): Promise<boolean>

Example:

import { RendererSelectMode } from '@seatmap.pro/renderer';

await renderer.selectSeatBlock(anchorSeatId, focusSeatId);

await renderer.selectSeatBlock(anchorSeatId, focusSeatId, RendererSelectMode.ADD);

Parameters:

Parameter Type Required Default Description
anchorSeatId number Yes - Seat where the block starts
focusSeatId number Yes - Seat where the block ends
mode RendererSelectMode No RendererSelectMode.REPLACE Selection algebra applied to the block

Returns: Promise<boolean> - false when either seat is unknown, the two seats belong to different sections, or grid coordinates are unavailable for that section; true otherwise.

Behavior:

  • REPLACE swaps the current selection for the block, ADD unions with it, SUBTRACT removes the block from it.
  • Grid coordinates are fetched for the anchor’s section on demand, so the first call for a section may await a request.

Backend dependency: untransformed grid coordinates come from dedicated per-section endpoints (event/grid/ and event/schema/grid/), which the admin renderer calls for itself and caches per section. No integration change is required.


Configurable hotkeys (SEAT-1047)

Status: New

Description: Keyboard input in the admin renderer is routed through a hotkey controller with configurable bindings. Keystrokes are handled while the pointer is over the stage, and ignored when the event target is an input, textarea, select, or any editable element.

API:

export type HotkeysSetting = false | Partial<Record<string, string | null>>;

export type AdminHotkeyAction =
  | 'pan'
  | 'clearSelection'
  | 'selectAll'
  | 'zoomIn'
  | 'zoomOut'
  | 'zoomToFit'
  | 'flatSection';

interface IRendererSettings {
  hotkeys?: HotkeysSetting;
}

Default bindings:

Action id Combo Type
pan Space hold
clearSelection Escape press
selectAll Mod+A press
zoomIn Mod+= press
zoomOut Mod+- press
zoomToFit Mod+0 press
flatSection F hold

Example:

const renderer = new SeatmapAdminRenderer(document.getElementById('renderer-container'), {
  publicKey: 'your-public-key',
  hotkeys: {
    selectAll: 'Mod+E',
    flatSection: null,
  },
});

Behavior:

  • A string value replaces that action’s default combo; null disables the action; omitted actions keep their defaults.
  • hotkeys: false disables keyboard handling entirely, for host applications that own the keyboard.
  • A combo is an optional Mod+ prefix plus a single key: a letter, a digit, =, -, or a key code such as Space or Escape. Mod maps to Cmd on macOS and Ctrl elsewhere.
  • A hold binding is active only while the key is held. Space pans temporarily and restores the previous mode on release; F flattens the hovered section for the duration of the hold.
  • Held keys are released on window blur, so a key does not stay stuck when focus leaves the page.
  • hotkeys lives on the shared IRendererSettings, but only the admin renderer registers bindings. Setting it on a booking renderer has no effect.

Selection methods on the admin renderer (SEAT-1047)

Status: New

Description: The two selection actions behind the clearSelection and selectAll hotkeys are also public methods, so a host application can drive them from its own UI without binding a key.

API:

clearSelection(): void;
selectAllSeats(): void;

Behavior:

  • clearSelection clears the seat selection, resets the selected outline state, and clears section selection, firing onSectionsSelectionChange with an empty array when sections were selected.
  • selectAllSeats selects every seat that is not filtered out of the current view.

Per-source section outline styling (SEAT-985)

Status: New

Description: Section outline styles previously applied uniformly to every outline, so a hover style meant for user-drawn zones also changed the auto-generated outlines around seat sections. A bySource map now overrides the flat styles for one outline source only.

API:

export interface ISvgSectionStateStyles {
  default?: Pick<ISvgSectionStyle, 'sectionName' | 'stroke' | 'cursor' | 'bgColor'>;
  unavailable?: ISvgSectionStyle;
  filtered?: ISvgSectionStyle;
  hovered?: ISvgSectionStyle;
  selected?: ISvgSectionStyle;
}

export interface IRendererSvgSectionStylesSetting extends ISvgSectionStateStyles {
  bySource?: Partial<Record<'svg' | 'shape' | 'auto' | 'fallback', ISvgSectionStateStyles>>;
}

Example:

const renderer = new SeatmapBookingRenderer(container, {
  publicKey: 'your-public-key',
  theme: {
    svgSectionStyles: {
      hovered: {
        stroke: { color: '#2196F3', width: '3px' },
      },
      bySource: {
        fallback: {
          hovered: { stroke: { color: 'transparent', width: '0' } },
        },
        auto: {
          hovered: { stroke: { color: 'transparent', width: '0' } },
        },
      },
    },
  },
});

Outline sources:

Source Origin
svg Zones drawn in the schema editor
shape Shape objects
auto Editor-generated outlines from seat groups
fallback Runtime-generated from seat positions

Behavior:

  • The flat configuration is unchanged and existing themes keep working. bySource is applied on top, per source.
  • bySource governs the SVG outline styling. In WebGL overlay mode the section hover ring colour still comes from the flat hovered.stroke.color.
  • The state keys inside a bySource entry are the same as the flat ones, so a source entry can override a single state and inherit the rest.

Related Files:

  • products/booking-client/src/models.ts - NEW: ISvgSectionStateStyles, bySource on IRendererSvgSectionStylesSetting

Branding overlay settings (SEAT-1072)

Status: New

Description: The booking service resolves a branding level per render from the organization that owns the public key, and the renderer applies it as an overlay above the canvas in both the WebGL and Canvas2D paths. An active organization on a paid plan renders with no branding; an active organization without a paid plan gets a small clickable “Powered by Seatmap.pro” badge; a suspended organization gets a blocking overlay that disables seat selection. These settings control the presentation, not the level.

API:

export interface IWatermarkSettings {
  position?: 'bottom-left' | 'bottom-right';
  interruptionTitle?: string;
  interruptionSubtitle?: string;
}

interface IRendererSettings {
  watermark?: IWatermarkSettings;
}

Example:

const renderer = new SeatmapBookingRenderer(container, {
  publicKey: 'your-public-key',
  watermark: {
    position: 'bottom-left',
    interruptionTitle: 'Booking is temporarily unavailable',
    interruptionSubtitle: 'Please contact support@example.com',
  },
});

Behavior:

  • Without position, the badge sits bottom-right, and moves to bottom-left when the minimap occupies bottom-right so the two never overlap. An explicit position always wins.
  • interruptionTitle and interruptionSubtitle replace the default copy on the blocking overlay. Each falls back to the default when omitted; the overlay blocks selection regardless of the text.
  • Branding resolution fails open. Any error resolving the organization renders with no branding, so it cannot break a booking flow.
  • Self-hosted deployments can force branding off with the SEATMAP_WATERMARK_DISABLED environment variable on the booking service, or the booking.config.seatmap.watermark.disabled Helm value.

Modified Features

Numeric prices preserve decimals (SEAT-1056)

Status: Warning - Modified

What Changed: The renderer derives the numeric price it reports from a price’s display label. That derivation now preserves decimals.

Before (v1.69.x):

// price label "12.50"
onSeatsSelect: (seats) => {
  console.log(seats[0].price); // 12
};

After (v1.70.0):

// price label "12.50"
onSeatsSelect: (seats) => {
  console.log(seats[0].price); // 12.5
};

Affected values: price on section-click and section-selection callbacks, and price on cart seats and general-admission entries (onSeatsSelect, getCart).

Backward Compatibility: The type is unchanged - a reported price is still either a finite number or undefined. A non-numeric label such as Gold still reports undefined, and a 0 label still reports 0. Integrations that assumed integer-only prices, or that re-parsed the label themselves, should review these values.

Related correctness fixes in the same area:

  • Cart price-id backfill matches a cart price to its price entry numerically, so decimal and suffixed labels (for example 12.50, 42 EUR) are matched correctly.
  • A general-admission entry whose stored price is not a finite number can once again be removed from the cart.
  • Restoring a seat from a saved cart key resolves a section by name, so a section whose name happens to be numeric is no longer confused with a different section that has that numeric id.
  • Loading a single section trims the price list to that section, so seat and group price assignments from other sections are no longer carried into the isolated view.

Animation completion reports whether it finished (SEAT-1055)

Status: Warning - Modified

What Changed: IRendererAnimation.onComplete now receives a flag telling you whether the animation ran to its natural end.

Before (v1.69.x):

onComplete?: () => void;

After (v1.70.0):

onComplete?: (completed: boolean) => void;

Backward Compatibility: YES. A zero-argument callback remains assignable to the new signature in TypeScript and ignores the extra argument at runtime, so existing code compiles and behaves as before.

Behavior: every transform animation now signals completion on every exit path - natural finish, cancel, or preemption - with completed set to false when it was interrupted. An awaited zoom sequence interrupted by a gesture stops cleanly instead of leaving a pending promise or skipping its snapback.


Bug Fixes

Label styling was silently ignored (SEAT-1069)

Issue: A schema’s labelStyle arrives on the wire as a JSON string and was never parsed, so every setting it carries - hiding the general-admission label, hiding the price dot, section label styling - had no effect.

Impact: Every integration relying on labelStyle.

Root Cause: the section DTO aliased the internal model, so the string-typed wire field was read as if it were already an object.

Fix: labelStyle is parsed at schema ingest.

Behavior Change:

  • Before: labelStyle settings were dropped.
  • After: labelStyle settings apply.

User Action Required: NO - but a schema that carried a labelStyle will now render as that style specifies, which may differ from what you saw before.


Zoom to a section jerked or froze (SEAT-1077)

Issue: During a zoom, two independent draw drivers painted the WebGL stage at different transforms, which showed as a two-step jerk, and a detail-crop upload running mid-animation blocked the main thread.

Fix: the self-driven redraw loop is gated while a transform animation runs, so the animator is the sole painter, and a pending viewport settle is cancelled when an animation starts. Concurrent zoom-in and zoom-out requests during an in-progress zoom are queued rather than interleaved.

User Action Required: NO.


Zoom and section transitions settled incorrectly (SEAT-1055)

Issue: Programmatic zoom-to-destination committed the raw target rather than the pan-limited position, so a zoom into a point near the venue edge rested past the bounds. Section-view transitions used a fixed timer instead of waiting for the zoom-to-fit animation, so the rotate-in started early or late on slower devices.

Fix:

  • Zoom-to-destination, including minimap clicks and 2D zoom-to-section, commits the pan-limited position. A pinch-to-zoom that ends out of bounds snaps back to the limit.
  • Entering and exiting a section view waits for the zoom-to-fit animation to finish.
  • A section whose geometry produces an undefined rotation angle logs a warning and resets cleanly instead of animating a NaN transform. Overlapping section rotations no longer corrupt each other’s state.
  • A zero-duration zoom commits in a single step with no blank or NaN frame, and the pan-limit snapback runs even while a zoom animation is in flight.

User Action Required: NO.


Type Definitions

New exported types

export type { HotkeysSetting } from './HotkeyController';
export type { AdminHotkeyAction } from './admin-renderer/hotkeys';
export type { IAdminRenderer } from './admin-renderer';

export interface IWatermarkSettings { ... }
export interface ISvgSectionStateStyles { ... }

IAdminRenderer was previously internal and is now exported alongside IAdminRendererSettings, so a host application can type a reference to the admin renderer by its interface.

Modified types

export interface IRendererSettings {
  hotkeys?: HotkeysSetting; // Added
  watermark?: IWatermarkSettings; // Added
}

export interface IRendererSvgSectionStylesSetting extends ISvgSectionStateStyles {
  bySource?: Partial<Record<OutlineSource, ISvgSectionStateStyles>>; // Added
}

export interface IRendererAnimation {
  onComplete?: (completed: boolean) => void; // Changed from () => void
}

The outline source union is 'svg' | 'shape' | 'auto' | 'fallback'. It is reachable structurally through bySource, so a TypeScript object literal with those keys type-checks without importing the alias.


Performance Changes

No renderer method signatures, configuration options, or callback values changed for performance work. Two areas improved:

Large venues (SEAT-1078): data operations that re-scanned every seat or section now use internal indexes. Seat filtering, locking and state changes no longer rebuild the spatial hit-test index; minimap cart pins are drawn from the cart rather than by scanning all seats; key-based seat lookups resolve in constant time per key. Per frame, row geometry uploads only when it changes, the static background quad and per-seat selection and hover data are no longer re-sent each frame, and the “is any seat still loading” check runs in constant time. GPU shaders, textures and buffers are released more thoroughly on teardown, reducing memory across repeated map loads.

Backgrounds (SEAT-1081): the converter emits a chain of progressively smaller levels alongside the tile grid and encodes tiles as WebP. The renderer picks the smallest level that serves the requested region, fetches its tiles in parallel and caches them, and re-checks each level’s decoded width so a level that under-delivers steps up. On a 9500px stadium background the worst case drops from about 2.5 MB to about 400 KB per crop.


Migration Guide

Quick Migration

From v1.69.x to v1.70.0

  1. Update Package

    npm install @seatmap.pro/renderer@1.70.0
    # or
    yarn add @seatmap.pro/renderer@1.70.0
    
  2. Review numeric prices

    If your integration reads price from a callback or from the cart and assumed whole numbers, confirm it handles decimals. Nothing else requires a code change.

Compatibility

  • All existing code continues to work
  • No TypeScript errors
  • No configuration changes required

Examples

Admin tooling with the new APIs

import { SeatmapAdminRenderer, RendererSelectMode } from '@seatmap.pro/renderer';

const renderer = new SeatmapAdminRenderer(document.getElementById('seatmap'), {
  publicKey: 'your-public-key',
  hotkeys: {
    selectAll: 'Mod+E',
  },
  onFlatSectionViewChange: (sectionId) => {
    setBreadcrumb(sectionId === null ? 'Venue' : sectionLabel(sectionId));
  },
  onSectionsSelectionChange: (sections) => {
    setSelectedSections(sections);
  },
});

await renderer.loadEvent('event-123');

renderer.setMode('selectSections');

async function inspectSection(sectionId) {
  const flattened = await renderer.setFlatSectionView(sectionId);
  if (!flattened) {
    showToast('That section cannot be flattened');
  }
}

async function selectRange(anchorSeatId, focusSeatId, additive) {
  await renderer.selectSeatBlock(
    anchorSeatId,
    focusSeatId,
    additive ? RendererSelectMode.ADD : RendererSelectMode.REPLACE,
  );
}

async function backToVenue() {
  await renderer.setFlatSectionView(null);
}


Support

Questions or Issues?

Reporting Bugs

Include:

  • Renderer version: 1.70.0
  • Browser and version
  • Minimal reproduction code
  • Console errors
  • Steps to reproduce