@kerusiweb/angular 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
1
2
  import * as i0 from '@angular/core';
2
- import { input, output, computed, ChangeDetectionStrategy, Component, inject, ElementRef, model, signal, effect } from '@angular/core';
3
- import { DEFAULT_KERUSI_COLORS, seatFill, seatTextFill, cssVar, seatBodyPath, seatOccupantVariant, seatSelectedFrame, seatCoreFill, seatWashFill, seatOccupantPath, seatOccupantStroke, occupantFillFor, occupantStrokeFor, elementStyle, elementFill, elementTextFill, screenPath, readableOn, formatMoney, DEFAULT_SEAT_ARIA_STRINGS, errorsOf, KerusiValidationError, validateDocumentSet, expireHolds, buildRenderModel, resolveMapLocale, isRtlLocale, resolveColors, computeSectionLayout, buildNavigationGraph, seatAriaLabel, summarizeSelection, toggleSeatSelection, disallowedAnnouncement, DEFAULT_SEQUENCE_READER, heldSeats, applyStateDeltaOrdered } from '@kerusiweb/core';
3
+ import { input, output, computed, ChangeDetectionStrategy, Component, inject, ElementRef, model, signal, viewChild, effect, untracked } from '@angular/core';
4
+ import { DEFAULT_KERUSI_COLORS, seatFill, seatTextFill, cssVar, seatBodyPath, seatOccupantVariant, seatSelectedFrame, seatCoreFill, seatWashFill, seatOccupantPath, seatOccupantStroke, occupantFillFor, occupantStrokeFor, elementStyle, elementFill, elementTextFill, screenPath, readableOn, formatMoney, DEFAULT_SEAT_ARIA_STRINGS, DEFAULT_MAX_ZOOM, FIT_VIEWPORT, IDLE_GESTURE, errorsOf, KerusiValidationError, viewportsEqual, validateDocumentSet, expireHolds, buildRenderModel, resolveMapLocale, isRtlLocale, resolveColors, zoomLimits, resolveControlStrings, controlStyle, viewportTransform, viewportTouchAction, computeSectionLayout, buildNavigationGraph, seatAriaLabel, summarizeSelection, toggleSeatSelection, disallowedAnnouncement, fitViewport, stepViewport, clampViewport, revealRect, reduceGesture, wheelZoomFactor, zoomViewportBy, zoomViewportTo, isTextEntryTarget, keyboardViewport, DEFAULT_SEQUENCE_READER, heldSeats, applyStateDeltaOrdered } from '@kerusiweb/core';
4
5
 
5
6
  /**
6
7
  * Draws one Kerusi `Section` as a single `<svg>`.
@@ -198,6 +199,15 @@ class KerusiSectionComponent {
198
199
  style(placed) {
199
200
  return elementStyle(placed.element.kind);
200
201
  }
202
+ /** A text element with no label draws nothing, rather than an empty box. */
203
+ drawsElement(placed) {
204
+ const style = this.style(placed);
205
+ return style.shape !== 'text' || (!!placed.element.label && style.showLabel);
206
+ }
207
+ /** A label on a shape stays upright; a text element's own label is what rotates. */
208
+ labelTransform(placed) {
209
+ return this.style(placed).shape === 'text' ? null : this.counterRotate(placed);
210
+ }
201
211
  elementFillOf(placed) {
202
212
  return elementFill(this.style(placed).tone, this.colors());
203
213
  }
@@ -228,11 +238,11 @@ class KerusiSectionComponent {
228
238
  }
229
239
  }
230
240
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
231
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiSectionComponent, isStandalone: true, selector: "kerusi-section", inputs: { section: { classPropertyName: "section", publicName: "section", isSignal: true, isRequired: true, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: true, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, focusedSeatId: { classPropertyName: "focusedSeatId", publicName: "focusedSeatId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabels: { classPropertyName: "ariaLabels", publicName: "ariaLabels", isSignal: true, isRequired: false, transformFunction: null }, colors: { classPropertyName: "colors", publicName: "colors", isSignal: true, isRequired: false, transformFunction: null }, typeColors: { classPropertyName: "typeColors", publicName: "typeColors", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, unitScale: { classPropertyName: "unitScale", publicName: "unitScale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { seatActivate: "seatActivate", seatFocused: "seatFocused", seatKeydown: "seatKeydown" }, ngImport: i0, template: "<svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"kerusi-section-svg\"\r\n role=\"presentation\"\r\n focusable=\"false\"\r\n [style.--kerusi-focus-ring-input]=\"colors().focusRing\"\r\n [attr.viewBox]=\"viewBox()\"\r\n [style.max-width.px]=\"naturalWidth()\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n>\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n [attr.width]=\"layout().width\"\r\n [attr.height]=\"layout().height\"\r\n [style.fill]=\"backdrop()\"\r\n />\r\n\r\n <!-- Elements first, so a screen or stage sits behind the seats it faces. -->\r\n @for (placed of layout().elements; track placed.element.id) {\r\n <g\r\n class=\"kerusi-element kerusi-element--{{ placed.element.kind }}\"\r\n [attr.transform]=\"transform(placed)\"\r\n >\r\n @switch (style(placed).shape) {\r\n @case ('screen') {\r\n <path [attr.d]=\"screenPathOf(placed)\" [style.fill]=\"elementFillOf(placed)\" />\r\n }\r\n @case ('void') {\r\n <rect\r\n class=\"kerusi-element__void\"\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [style.stroke]=\"elementTextFillOf(placed)\"\r\n fill=\"none\"\r\n />\r\n }\r\n @default {\r\n <rect\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [attr.rx]=\"radiusOf(placed)\"\r\n [style.fill]=\"elementFillOf(placed)\"\r\n />\r\n }\r\n }\r\n @if (placed.element.label && style(placed).showLabel) {\r\n <text\r\n class=\"kerusi-element__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"elementTextFillOf(placed)\"\r\n [attr.transform]=\"counterRotate(placed)\"\r\n >\r\n {{ placed.element.label }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n\r\n @for (placed of layout().seats; track placed.seat.id) {\r\n <g\r\n [class]=\"seatClasses(placed)\"\r\n role=\"button\"\r\n [attr.tabindex]=\"tabIndex(placed)\"\r\n [attr.aria-label]=\"ariaLabel(placed)\"\r\n [attr.aria-pressed]=\"isSelected(placed)\"\r\n [attr.aria-disabled]=\"!placed.seat.selectable\"\r\n [attr.data-seat-id]=\"placed.seat.id\"\r\n [attr.transform]=\"transform(placed)\"\r\n (click)=\"onSeatClick(placed)\"\r\n (focus)=\"onSeatFocus(placed)\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n @let variant = occupantVariant(placed);\r\n <!-- Tapered toward the back, square at the front, so which way the seat\r\n faces is legible from the outline alone. -->\r\n <path class=\"kerusi-seat__box\" [attr.d]=\"bodyPath(placed)\" [style.fill]=\"fill(placed)\" />\r\n @if (variant === 'held' || variant === 'booked') {\r\n <!-- Colour stays the seat type's; a taken seat recedes under a wash\r\n instead, so the type colour still reads on a busy map. -->\r\n <path\r\n class=\"kerusi-seat__wash\"\r\n [attr.d]=\"bodyPath(placed)\"\r\n [style.fill]=\"washFill(placed)\"\r\n />\r\n }\r\n @if (variant === 'selected') {\r\n <!-- Selection is the one state drawn from two tones: the body above is\r\n the frame, this is the bright core inside it. Whichever way the\r\n host page is themed, one of the two separates from it \u2014 which is\r\n why nothing here has to know about the page. -->\r\n <path class=\"kerusi-seat__core\" [attr.d]=\"corePath(placed)\" [style.fill]=\"coreFill()\" />\r\n }\r\n @if (variant) {\r\n <!-- Deliberately not counter-rotated: the occupant leans with the\r\n seat. Solid = settled (yours, or sold); hollow = a hold still in\r\n progress. -->\r\n <path\r\n class=\"kerusi-seat__occupant kerusi-seat__occupant--{{ variant }}\"\r\n [attr.d]=\"occupantPath(placed)\"\r\n [attr.stroke-width]=\"occupantStroke(placed)\"\r\n [style.fill]=\"occupantFill(variant)\"\r\n [style.stroke]=\"occupantStrokeColor(variant)\"\r\n />\r\n }\r\n <text\r\n class=\"kerusi-seat__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"textFill(placed)\"\r\n [style.stroke]=\"labelHalo(placed)\"\r\n [attr.stroke-width]=\"labelHaloWidth(placed)\"\r\n [attr.transform]=\"counterRotate(placed)\"\r\n >\r\n {{ placed.seat.label }}\r\n </text>\r\n @if (placed.seat.accessibility?.wheelchairAccessible) {\r\n <!-- A corner marker, not a different fill: availability owns the fill. -->\r\n <circle\r\n class=\"kerusi-seat__wheelchair\"\r\n [attr.cx]=\"wheelchairCx(placed)\"\r\n [attr.cy]=\"wheelchairCy(placed)\"\r\n [attr.r]=\"wheelchairRadius(placed)\"\r\n [style.fill]=\"textFill(placed)\"\r\n />\r\n }\r\n </g>\r\n }\r\n</svg>\r\n", styles: [":host{display:block}.kerusi-section-svg{display:block;width:100%;height:auto;max-width:100%}.kerusi-seat{cursor:pointer;outline:none}.kerusi-seat--unselectable{cursor:not-allowed}.kerusi-seat__label,.kerusi-element__label{pointer-events:none;-webkit-user-select:none;user-select:none;font-family:inherit}.kerusi-seat__label{paint-order:stroke;stroke-linejoin:round}.kerusi-seat__wash{pointer-events:none;opacity:.5}.kerusi-seat__core,.kerusi-seat__occupant{pointer-events:none}.kerusi-seat__occupant--selected{opacity:.2}.kerusi-seat__occupant--booked{opacity:.55}.kerusi-seat__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-seat--selected .kerusi-seat__label{font-weight:600}.kerusi-seat:focus-visible .kerusi-seat__box{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5;paint-order:stroke}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
241
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiSectionComponent, isStandalone: true, selector: "kerusi-section", inputs: { section: { classPropertyName: "section", publicName: "section", isSignal: true, isRequired: true, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: true, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, focusedSeatId: { classPropertyName: "focusedSeatId", publicName: "focusedSeatId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabels: { classPropertyName: "ariaLabels", publicName: "ariaLabels", isSignal: true, isRequired: false, transformFunction: null }, colors: { classPropertyName: "colors", publicName: "colors", isSignal: true, isRequired: false, transformFunction: null }, typeColors: { classPropertyName: "typeColors", publicName: "typeColors", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, unitScale: { classPropertyName: "unitScale", publicName: "unitScale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { seatActivate: "seatActivate", seatFocused: "seatFocused", seatKeydown: "seatKeydown" }, ngImport: i0, template: "<svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"kerusi-section-svg\"\r\n role=\"presentation\"\r\n focusable=\"false\"\r\n [style.--kerusi-focus-ring-input]=\"colors().focusRing\"\r\n [attr.viewBox]=\"viewBox()\"\r\n [style.max-width.px]=\"naturalWidth()\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n>\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n [attr.width]=\"layout().width\"\r\n [attr.height]=\"layout().height\"\r\n [style.fill]=\"backdrop()\"\r\n />\r\n\r\n <!-- Elements first, so a screen or stage sits behind the seats it faces. -->\r\n @for (placed of layout().elements; track placed.element.id) {\r\n @if (drawsElement(placed)) {\r\n <g\r\n class=\"kerusi-element kerusi-element--{{ placed.element.kind }}\"\r\n [attr.transform]=\"transform(placed)\"\r\n >\r\n @switch (style(placed).shape) {\r\n @case ('screen') {\r\n <path [attr.d]=\"screenPathOf(placed)\" [style.fill]=\"elementFillOf(placed)\" />\r\n }\r\n @case ('text') {}\r\n @case ('void') {\r\n <rect\r\n class=\"kerusi-element__void\"\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [style.stroke]=\"elementTextFillOf(placed)\"\r\n fill=\"none\"\r\n />\r\n }\r\n @default {\r\n <rect\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [attr.rx]=\"radiusOf(placed)\"\r\n [style.fill]=\"elementFillOf(placed)\"\r\n />\r\n }\r\n }\r\n @if (placed.element.label && style(placed).showLabel) {\r\n <text\r\n class=\"kerusi-element__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"elementTextFillOf(placed)\"\r\n [attr.transform]=\"labelTransform(placed)\"\r\n >\r\n {{ placed.element.label }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n }\r\n\r\n @for (placed of layout().seats; track placed.seat.id) {\r\n <g\r\n [class]=\"seatClasses(placed)\"\r\n role=\"button\"\r\n [attr.tabindex]=\"tabIndex(placed)\"\r\n [attr.aria-label]=\"ariaLabel(placed)\"\r\n [attr.aria-pressed]=\"isSelected(placed)\"\r\n [attr.aria-disabled]=\"!placed.seat.selectable\"\r\n [attr.data-seat-id]=\"placed.seat.id\"\r\n [attr.transform]=\"transform(placed)\"\r\n (click)=\"onSeatClick(placed)\"\r\n (focus)=\"onSeatFocus(placed)\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n @let variant = occupantVariant(placed);\r\n <!-- Tapered toward the back, square at the front, so which way the seat\r\n faces is legible from the outline alone. -->\r\n <path class=\"kerusi-seat__box\" [attr.d]=\"bodyPath(placed)\" [style.fill]=\"fill(placed)\" />\r\n @if (variant === 'held' || variant === 'booked') {\r\n <!-- Colour stays the seat type's; a taken seat recedes under a wash\r\n instead, so the type colour still reads on a busy map. -->\r\n <path\r\n class=\"kerusi-seat__wash\"\r\n [attr.d]=\"bodyPath(placed)\"\r\n [style.fill]=\"washFill(placed)\"\r\n />\r\n }\r\n @if (variant === 'selected') {\r\n <!-- Selection is the one state drawn from two tones: the body above is\r\n the frame, this is the bright core inside it. Whichever way the\r\n host page is themed, one of the two separates from it \u2014 which is\r\n why nothing here has to know about the page. -->\r\n <path class=\"kerusi-seat__core\" [attr.d]=\"corePath(placed)\" [style.fill]=\"coreFill()\" />\r\n }\r\n @if (variant) {\r\n <!-- Deliberately not counter-rotated: the occupant leans with the\r\n seat. Solid = settled (yours, or sold); hollow = a hold still in\r\n progress. -->\r\n <path\r\n class=\"kerusi-seat__occupant kerusi-seat__occupant--{{ variant }}\"\r\n [attr.d]=\"occupantPath(placed)\"\r\n [attr.stroke-width]=\"occupantStroke(placed)\"\r\n [style.fill]=\"occupantFill(variant)\"\r\n [style.stroke]=\"occupantStrokeColor(variant)\"\r\n />\r\n }\r\n <text\r\n class=\"kerusi-seat__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"textFill(placed)\"\r\n [style.stroke]=\"labelHalo(placed)\"\r\n [attr.stroke-width]=\"labelHaloWidth(placed)\"\r\n [attr.transform]=\"counterRotate(placed)\"\r\n >\r\n {{ placed.seat.label }}\r\n </text>\r\n @if (placed.seat.accessibility?.wheelchairAccessible) {\r\n <!-- A corner marker, not a different fill: availability owns the fill. -->\r\n <circle\r\n class=\"kerusi-seat__wheelchair\"\r\n [attr.cx]=\"wheelchairCx(placed)\"\r\n [attr.cy]=\"wheelchairCy(placed)\"\r\n [attr.r]=\"wheelchairRadius(placed)\"\r\n [style.fill]=\"textFill(placed)\"\r\n />\r\n }\r\n </g>\r\n }\r\n</svg>\r\n", styles: [":host{display:block}.kerusi-section-svg{display:block;width:100%;height:auto;max-width:100%}.kerusi-seat{cursor:pointer;outline:none}.kerusi-seat--unselectable{cursor:not-allowed}.kerusi-seat__label,.kerusi-element__label{pointer-events:none;-webkit-user-select:none;user-select:none;font-family:inherit}.kerusi-seat__label{paint-order:stroke;stroke-linejoin:round}.kerusi-seat__wash{pointer-events:none;opacity:.5}.kerusi-seat__core,.kerusi-seat__occupant{pointer-events:none}.kerusi-seat__occupant--selected{opacity:.2}.kerusi-seat__occupant--booked{opacity:.55}.kerusi-seat__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-seat--selected .kerusi-seat__label{font-weight:600}.kerusi-seat:focus-visible .kerusi-seat__box{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5;paint-order:stroke}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
232
242
  }
233
243
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSectionComponent, decorators: [{
234
244
  type: Component,
235
- args: [{ selector: 'kerusi-section', changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"kerusi-section-svg\"\r\n role=\"presentation\"\r\n focusable=\"false\"\r\n [style.--kerusi-focus-ring-input]=\"colors().focusRing\"\r\n [attr.viewBox]=\"viewBox()\"\r\n [style.max-width.px]=\"naturalWidth()\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n>\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n [attr.width]=\"layout().width\"\r\n [attr.height]=\"layout().height\"\r\n [style.fill]=\"backdrop()\"\r\n />\r\n\r\n <!-- Elements first, so a screen or stage sits behind the seats it faces. -->\r\n @for (placed of layout().elements; track placed.element.id) {\r\n <g\r\n class=\"kerusi-element kerusi-element--{{ placed.element.kind }}\"\r\n [attr.transform]=\"transform(placed)\"\r\n >\r\n @switch (style(placed).shape) {\r\n @case ('screen') {\r\n <path [attr.d]=\"screenPathOf(placed)\" [style.fill]=\"elementFillOf(placed)\" />\r\n }\r\n @case ('void') {\r\n <rect\r\n class=\"kerusi-element__void\"\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [style.stroke]=\"elementTextFillOf(placed)\"\r\n fill=\"none\"\r\n />\r\n }\r\n @default {\r\n <rect\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [attr.rx]=\"radiusOf(placed)\"\r\n [style.fill]=\"elementFillOf(placed)\"\r\n />\r\n }\r\n }\r\n @if (placed.element.label && style(placed).showLabel) {\r\n <text\r\n class=\"kerusi-element__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"elementTextFillOf(placed)\"\r\n [attr.transform]=\"counterRotate(placed)\"\r\n >\r\n {{ placed.element.label }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n\r\n @for (placed of layout().seats; track placed.seat.id) {\r\n <g\r\n [class]=\"seatClasses(placed)\"\r\n role=\"button\"\r\n [attr.tabindex]=\"tabIndex(placed)\"\r\n [attr.aria-label]=\"ariaLabel(placed)\"\r\n [attr.aria-pressed]=\"isSelected(placed)\"\r\n [attr.aria-disabled]=\"!placed.seat.selectable\"\r\n [attr.data-seat-id]=\"placed.seat.id\"\r\n [attr.transform]=\"transform(placed)\"\r\n (click)=\"onSeatClick(placed)\"\r\n (focus)=\"onSeatFocus(placed)\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n @let variant = occupantVariant(placed);\r\n <!-- Tapered toward the back, square at the front, so which way the seat\r\n faces is legible from the outline alone. -->\r\n <path class=\"kerusi-seat__box\" [attr.d]=\"bodyPath(placed)\" [style.fill]=\"fill(placed)\" />\r\n @if (variant === 'held' || variant === 'booked') {\r\n <!-- Colour stays the seat type's; a taken seat recedes under a wash\r\n instead, so the type colour still reads on a busy map. -->\r\n <path\r\n class=\"kerusi-seat__wash\"\r\n [attr.d]=\"bodyPath(placed)\"\r\n [style.fill]=\"washFill(placed)\"\r\n />\r\n }\r\n @if (variant === 'selected') {\r\n <!-- Selection is the one state drawn from two tones: the body above is\r\n the frame, this is the bright core inside it. Whichever way the\r\n host page is themed, one of the two separates from it \u2014 which is\r\n why nothing here has to know about the page. -->\r\n <path class=\"kerusi-seat__core\" [attr.d]=\"corePath(placed)\" [style.fill]=\"coreFill()\" />\r\n }\r\n @if (variant) {\r\n <!-- Deliberately not counter-rotated: the occupant leans with the\r\n seat. Solid = settled (yours, or sold); hollow = a hold still in\r\n progress. -->\r\n <path\r\n class=\"kerusi-seat__occupant kerusi-seat__occupant--{{ variant }}\"\r\n [attr.d]=\"occupantPath(placed)\"\r\n [attr.stroke-width]=\"occupantStroke(placed)\"\r\n [style.fill]=\"occupantFill(variant)\"\r\n [style.stroke]=\"occupantStrokeColor(variant)\"\r\n />\r\n }\r\n <text\r\n class=\"kerusi-seat__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"textFill(placed)\"\r\n [style.stroke]=\"labelHalo(placed)\"\r\n [attr.stroke-width]=\"labelHaloWidth(placed)\"\r\n [attr.transform]=\"counterRotate(placed)\"\r\n >\r\n {{ placed.seat.label }}\r\n </text>\r\n @if (placed.seat.accessibility?.wheelchairAccessible) {\r\n <!-- A corner marker, not a different fill: availability owns the fill. -->\r\n <circle\r\n class=\"kerusi-seat__wheelchair\"\r\n [attr.cx]=\"wheelchairCx(placed)\"\r\n [attr.cy]=\"wheelchairCy(placed)\"\r\n [attr.r]=\"wheelchairRadius(placed)\"\r\n [style.fill]=\"textFill(placed)\"\r\n />\r\n }\r\n </g>\r\n }\r\n</svg>\r\n", styles: [":host{display:block}.kerusi-section-svg{display:block;width:100%;height:auto;max-width:100%}.kerusi-seat{cursor:pointer;outline:none}.kerusi-seat--unselectable{cursor:not-allowed}.kerusi-seat__label,.kerusi-element__label{pointer-events:none;-webkit-user-select:none;user-select:none;font-family:inherit}.kerusi-seat__label{paint-order:stroke;stroke-linejoin:round}.kerusi-seat__wash{pointer-events:none;opacity:.5}.kerusi-seat__core,.kerusi-seat__occupant{pointer-events:none}.kerusi-seat__occupant--selected{opacity:.2}.kerusi-seat__occupant--booked{opacity:.55}.kerusi-seat__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-seat--selected .kerusi-seat__label{font-weight:600}.kerusi-seat:focus-visible .kerusi-seat__box{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5;paint-order:stroke}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"] }]
245
+ args: [{ selector: 'kerusi-section', changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"kerusi-section-svg\"\r\n role=\"presentation\"\r\n focusable=\"false\"\r\n [style.--kerusi-focus-ring-input]=\"colors().focusRing\"\r\n [attr.viewBox]=\"viewBox()\"\r\n [style.max-width.px]=\"naturalWidth()\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n>\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n [attr.width]=\"layout().width\"\r\n [attr.height]=\"layout().height\"\r\n [style.fill]=\"backdrop()\"\r\n />\r\n\r\n <!-- Elements first, so a screen or stage sits behind the seats it faces. -->\r\n @for (placed of layout().elements; track placed.element.id) {\r\n @if (drawsElement(placed)) {\r\n <g\r\n class=\"kerusi-element kerusi-element--{{ placed.element.kind }}\"\r\n [attr.transform]=\"transform(placed)\"\r\n >\r\n @switch (style(placed).shape) {\r\n @case ('screen') {\r\n <path [attr.d]=\"screenPathOf(placed)\" [style.fill]=\"elementFillOf(placed)\" />\r\n }\r\n @case ('text') {}\r\n @case ('void') {\r\n <rect\r\n class=\"kerusi-element__void\"\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [style.stroke]=\"elementTextFillOf(placed)\"\r\n fill=\"none\"\r\n />\r\n }\r\n @default {\r\n <rect\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [attr.rx]=\"radiusOf(placed)\"\r\n [style.fill]=\"elementFillOf(placed)\"\r\n />\r\n }\r\n }\r\n @if (placed.element.label && style(placed).showLabel) {\r\n <text\r\n class=\"kerusi-element__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"elementTextFillOf(placed)\"\r\n [attr.transform]=\"labelTransform(placed)\"\r\n >\r\n {{ placed.element.label }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n }\r\n\r\n @for (placed of layout().seats; track placed.seat.id) {\r\n <g\r\n [class]=\"seatClasses(placed)\"\r\n role=\"button\"\r\n [attr.tabindex]=\"tabIndex(placed)\"\r\n [attr.aria-label]=\"ariaLabel(placed)\"\r\n [attr.aria-pressed]=\"isSelected(placed)\"\r\n [attr.aria-disabled]=\"!placed.seat.selectable\"\r\n [attr.data-seat-id]=\"placed.seat.id\"\r\n [attr.transform]=\"transform(placed)\"\r\n (click)=\"onSeatClick(placed)\"\r\n (focus)=\"onSeatFocus(placed)\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n @let variant = occupantVariant(placed);\r\n <!-- Tapered toward the back, square at the front, so which way the seat\r\n faces is legible from the outline alone. -->\r\n <path class=\"kerusi-seat__box\" [attr.d]=\"bodyPath(placed)\" [style.fill]=\"fill(placed)\" />\r\n @if (variant === 'held' || variant === 'booked') {\r\n <!-- Colour stays the seat type's; a taken seat recedes under a wash\r\n instead, so the type colour still reads on a busy map. -->\r\n <path\r\n class=\"kerusi-seat__wash\"\r\n [attr.d]=\"bodyPath(placed)\"\r\n [style.fill]=\"washFill(placed)\"\r\n />\r\n }\r\n @if (variant === 'selected') {\r\n <!-- Selection is the one state drawn from two tones: the body above is\r\n the frame, this is the bright core inside it. Whichever way the\r\n host page is themed, one of the two separates from it \u2014 which is\r\n why nothing here has to know about the page. -->\r\n <path class=\"kerusi-seat__core\" [attr.d]=\"corePath(placed)\" [style.fill]=\"coreFill()\" />\r\n }\r\n @if (variant) {\r\n <!-- Deliberately not counter-rotated: the occupant leans with the\r\n seat. Solid = settled (yours, or sold); hollow = a hold still in\r\n progress. -->\r\n <path\r\n class=\"kerusi-seat__occupant kerusi-seat__occupant--{{ variant }}\"\r\n [attr.d]=\"occupantPath(placed)\"\r\n [attr.stroke-width]=\"occupantStroke(placed)\"\r\n [style.fill]=\"occupantFill(variant)\"\r\n [style.stroke]=\"occupantStrokeColor(variant)\"\r\n />\r\n }\r\n <text\r\n class=\"kerusi-seat__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"textFill(placed)\"\r\n [style.stroke]=\"labelHalo(placed)\"\r\n [attr.stroke-width]=\"labelHaloWidth(placed)\"\r\n [attr.transform]=\"counterRotate(placed)\"\r\n >\r\n {{ placed.seat.label }}\r\n </text>\r\n @if (placed.seat.accessibility?.wheelchairAccessible) {\r\n <!-- A corner marker, not a different fill: availability owns the fill. -->\r\n <circle\r\n class=\"kerusi-seat__wheelchair\"\r\n [attr.cx]=\"wheelchairCx(placed)\"\r\n [attr.cy]=\"wheelchairCy(placed)\"\r\n [attr.r]=\"wheelchairRadius(placed)\"\r\n [style.fill]=\"textFill(placed)\"\r\n />\r\n }\r\n </g>\r\n }\r\n</svg>\r\n", styles: [":host{display:block}.kerusi-section-svg{display:block;width:100%;height:auto;max-width:100%}.kerusi-seat{cursor:pointer;outline:none}.kerusi-seat--unselectable{cursor:not-allowed}.kerusi-seat__label,.kerusi-element__label{pointer-events:none;-webkit-user-select:none;user-select:none;font-family:inherit}.kerusi-seat__label{paint-order:stroke;stroke-linejoin:round}.kerusi-seat__wash{pointer-events:none;opacity:.5}.kerusi-seat__core,.kerusi-seat__occupant{pointer-events:none}.kerusi-seat__occupant--selected{opacity:.2}.kerusi-seat__occupant--booked{opacity:.55}.kerusi-seat__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-seat--selected .kerusi-seat__label{font-weight:600}.kerusi-seat:focus-visible .kerusi-seat__box{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5;paint-order:stroke}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"] }]
236
246
  }], propDecorators: { section: [{ type: i0.Input, args: [{ isSignal: true, alias: "section", required: true }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: true }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], focusedSeatId: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusedSeatId", required: false }] }], ariaLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabels", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], unitScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "unitScale", required: false }] }], seatActivate: [{ type: i0.Output, args: ["seatActivate"] }], seatFocused: [{ type: i0.Output, args: ["seatFocused"] }], seatKeydown: [{ type: i0.Output, args: ["seatKeydown"] }] } });
237
247
 
238
248
  /**
@@ -366,6 +376,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
366
376
  * ```html
367
377
  * <kerusi-seatmap [map]="map" [state]="state" [(selection)]="picked" />
368
378
  * ```
379
+ *
380
+ * Two opt-in features sit on top, both off by default so an existing map is
381
+ * unchanged: `[zoomable]` (pinch, wheel and drag, keys `+` `-` `0`, buttons)
382
+ * and `[maximizable]` (a full-screen button). Their arithmetic is in
383
+ * `@kerusiweb/core`; this class supplies the listeners and the signals.
369
384
  */
370
385
  class KerusiSeatmapComponent {
371
386
  host = inject((ElementRef));
@@ -447,6 +462,28 @@ class KerusiSeatmapComponent {
447
462
  ...(ngDevMode ? [{ debugName: "expireHolds" }] : /* istanbul ignore next */ []));
448
463
  expiryIntervalMs = input(1000, /* @ts-ignore */
449
464
  ...(ngDevMode ? [{ debugName: "expiryIntervalMs" }] : /* istanbul ignore next */ []));
465
+ // --- zoom, pan and maximize (both opt in) ---------------------------------
466
+ /**
467
+ * Pinch, Ctrl/Cmd + wheel and drag to zoom and pan, `+` `-` `0` on the keyboard,
468
+ * and on-screen buttons. A tap still selects at every zoom level.
469
+ */
470
+ zoomable = input(false, /* @ts-ignore */
471
+ ...(ngDevMode ? [{ debugName: "zoomable" }] : /* istanbul ignore next */ []));
472
+ /** The zoom ceiling, relative to fit-to-container. The floor is always fit. */
473
+ maxZoom = input(DEFAULT_MAX_ZOOM, /* @ts-ignore */
474
+ ...(ngDevMode ? [{ debugName: "maxZoom" }] : /* istanbul ignore next */ []));
475
+ /**
476
+ * The built-in zoom buttons. Turn this off to supply your own, anywhere on the
477
+ * page, and call `zoomIn()`, `zoomOut()` and `resetZoom()` on the component.
478
+ */
479
+ showZoomControls = input(true, /* @ts-ignore */
480
+ ...(ngDevMode ? [{ debugName: "showZoomControls" }] : /* istanbul ignore next */ []));
481
+ /** A button that maximizes the map to fill the screen. */
482
+ maximizable = input(false, /* @ts-ignore */
483
+ ...(ngDevMode ? [{ debugName: "maximizable" }] : /* istanbul ignore next */ []));
484
+ /** Overrides the built-in English names of the zoom and maximize controls. */
485
+ controlStrings = input({}, /* @ts-ignore */
486
+ ...(ngDevMode ? [{ debugName: "controlStrings" }] : /* istanbul ignore next */ []));
450
487
  // --- validation -----------------------------------------------------------
451
488
  validate = input('collect', /* @ts-ignore */
452
489
  ...(ngDevMode ? [{ debugName: "validate" }] : /* istanbul ignore next */ []));
@@ -457,6 +494,9 @@ class KerusiSeatmapComponent {
457
494
  seatFocus = output();
458
495
  /** Emitted whenever the documents change, in `collect` mode. */
459
496
  validationIssues = output();
497
+ /** The zoom and pan, whenever they change. For a host that persists or shows them. */
498
+ viewportChange = output();
499
+ maximizedChange = output();
460
500
  // --- internal state -------------------------------------------------------
461
501
  /**
462
502
  * The seat holding each section's tab stop, keyed by section id.
@@ -472,6 +512,25 @@ class KerusiSeatmapComponent {
472
512
  /** Bumped by the hold ticker to force the state to be re-derived. */
473
513
  expiryTick = signal(0, /* @ts-ignore */
474
514
  ...(ngDevMode ? [{ debugName: "expiryTick" }] : /* istanbul ignore next */ []));
515
+ viewportState = signal(FIT_VIEWPORT, /* @ts-ignore */
516
+ ...(ngDevMode ? [{ debugName: "viewportState" }] : /* istanbul ignore next */ []));
517
+ maximizedState = signal(false, /* @ts-ignore */
518
+ ...(ngDevMode ? [{ debugName: "maximizedState" }] : /* istanbul ignore next */ []));
519
+ /** True for a discrete change (button, key, reveal), false while a finger or wheel drives. */
520
+ animated = signal(false, /* @ts-ignore */
521
+ ...(ngDevMode ? [{ debugName: "animated" }] : /* istanbul ignore next */ []));
522
+ viewportEl = viewChild('viewport', /* @ts-ignore */
523
+ ...(ngDevMode ? [{ debugName: "viewportEl" }] : /* istanbul ignore next */ []));
524
+ contentEl = viewChild('content', /* @ts-ignore */
525
+ ...(ngDevMode ? [{ debugName: "contentEl" }] : /* istanbul ignore next */ []));
526
+ gesture = IDLE_GESTURE;
527
+ suppressClick = false;
528
+ gestureBase = null;
529
+ nativeFullscreen = false;
530
+ /** The current zoom and pan. Read-only: drive it with the methods below. */
531
+ viewport = this.viewportState.asReadonly();
532
+ /** Whether the map is currently maximized. */
533
+ maximized = this.maximizedState.asReadonly();
475
534
  constructor() {
476
535
  effect(() => {
477
536
  const issues = this.issues();
@@ -511,6 +570,93 @@ class KerusiSeatmapComponent {
511
570
  const handle = setInterval(() => this.expiryTick.update((n) => n + 1), this.expiryIntervalMs());
512
571
  onCleanup(() => clearInterval(handle));
513
572
  });
573
+ // Gesture listeners go on natively, not through the template: the wheel
574
+ // listener must be non-passive to `preventDefault` a page zoom, and the
575
+ // click listener must be in the capture phase to swallow the click that
576
+ // follows a drag before the seat sees it.
577
+ effect((onCleanup) => {
578
+ const el = this.viewportEl()?.nativeElement;
579
+ if (!el) {
580
+ return;
581
+ }
582
+ const listeners = [
583
+ ['pointerdown', (e) => this.onPointerDown(e), false],
584
+ ['pointermove', (e) => this.onPointerMove(e), false],
585
+ ['pointerup', (e) => this.onPointerEnd(e, 'up'), false],
586
+ ['pointercancel', (e) => this.onPointerEnd(e, 'cancel'), false],
587
+ ['wheel', (e) => this.onWheel(e), { passive: false }],
588
+ ['click', (e) => this.onClickCapture(e), true],
589
+ // Safari reports a trackpad pinch as gesture events, not Ctrl + wheel.
590
+ ['gesturestart', (e) => this.onGestureStart(e), { passive: false }],
591
+ ['gesturechange', (e) => this.onGestureChange(e), { passive: false }],
592
+ ['gestureend', (e) => this.onGestureEnd(e), { passive: false }],
593
+ ];
594
+ for (const [type, listener, options] of listeners) {
595
+ el.addEventListener(type, listener, options);
596
+ }
597
+ onCleanup(() => {
598
+ for (const [type, listener, options] of listeners) {
599
+ el.removeEventListener(type, listener, options);
600
+ }
601
+ });
602
+ });
603
+ // Re-clamp when the box or the ceiling changes: a rotated phone, a
604
+ // maximized map, a lowered `maxZoom`.
605
+ effect((onCleanup) => {
606
+ this.limits();
607
+ const view = this.viewportEl()?.nativeElement;
608
+ untracked(() => this.reclamp());
609
+ if (!view || typeof ResizeObserver === 'undefined') {
610
+ return;
611
+ }
612
+ const observer = new ResizeObserver(() => this.reclamp());
613
+ observer.observe(view);
614
+ const content = this.contentEl()?.nativeElement;
615
+ if (content) {
616
+ observer.observe(content);
617
+ }
618
+ onCleanup(() => observer.disconnect());
619
+ });
620
+ // Switching `zoomable` or `maximizable` off puts the map back as it was.
621
+ effect(() => {
622
+ if (!this.zoomable() && !viewportsEqual(untracked(this.viewportState), FIT_VIEWPORT)) {
623
+ this.viewportState.set(FIT_VIEWPORT);
624
+ }
625
+ });
626
+ effect(() => {
627
+ if (!this.maximizable() && untracked(this.maximizedState)) {
628
+ untracked(() => this.setMaximized(false));
629
+ }
630
+ });
631
+ // While maximized: freeze the page behind, and follow the browser leaving
632
+ // native full screen (Esc) so the two never disagree.
633
+ effect((onCleanup) => {
634
+ if (!this.maximizedState()) {
635
+ return;
636
+ }
637
+ const host = this.host.nativeElement;
638
+ const doc = host.ownerDocument;
639
+ const previousOverflow = doc.body.style.overflow;
640
+ doc.body.style.overflow = 'hidden';
641
+ const onFullscreenChange = () => {
642
+ if (doc.fullscreenElement === host) {
643
+ this.nativeFullscreen = true;
644
+ }
645
+ else if (this.nativeFullscreen) {
646
+ this.nativeFullscreen = false;
647
+ this.setMaximized(false);
648
+ }
649
+ };
650
+ doc.addEventListener('fullscreenchange', onFullscreenChange);
651
+ onCleanup(() => {
652
+ doc.body.style.overflow = previousOverflow;
653
+ doc.removeEventListener('fullscreenchange', onFullscreenChange);
654
+ this.nativeFullscreen = false;
655
+ if (doc.fullscreenElement === host) {
656
+ doc.exitFullscreen?.().catch(() => undefined);
657
+ }
658
+ });
659
+ });
514
660
  }
515
661
  // --- derived model --------------------------------------------------------
516
662
  issues = computed(() => this.validate() === 'off'
@@ -546,6 +692,22 @@ class KerusiSeatmapComponent {
546
692
  ...(ngDevMode ? [{ debugName: "resolvedColors" }] : /* istanbul ignore next */ []));
547
693
  selectionSet = computed(() => new Set(this.selection()), /* @ts-ignore */
548
694
  ...(ngDevMode ? [{ debugName: "selectionSet" }] : /* istanbul ignore next */ []));
695
+ limits = computed(() => zoomLimits(this.maxZoom()), /* @ts-ignore */
696
+ ...(ngDevMode ? [{ debugName: "limits" }] : /* istanbul ignore next */ []));
697
+ strings = computed(() => resolveControlStrings(this.controlStrings()), /* @ts-ignore */
698
+ ...(ngDevMode ? [{ debugName: "strings" }] : /* istanbul ignore next */ []));
699
+ controlColors = computed(() => controlStyle(this.colors()), /* @ts-ignore */
700
+ ...(ngDevMode ? [{ debugName: "controlColors" }] : /* istanbul ignore next */ []));
701
+ transform = computed(() => viewportTransform(this.viewportState()), /* @ts-ignore */
702
+ ...(ngDevMode ? [{ debugName: "transform" }] : /* istanbul ignore next */ []));
703
+ touchAction = computed(() => viewportTouchAction(this.viewportState(), this.maximizedState()), /* @ts-ignore */
704
+ ...(ngDevMode ? [{ debugName: "touchAction" }] : /* istanbul ignore next */ []));
705
+ showToolbar = computed(() => (this.zoomable() && this.showZoomControls()) || this.maximizable(), /* @ts-ignore */
706
+ ...(ngDevMode ? [{ debugName: "showToolbar" }] : /* istanbul ignore next */ []));
707
+ atMaxZoom = computed(() => this.viewportState().zoom >= this.limits().maxZoom - 1e-4, /* @ts-ignore */
708
+ ...(ngDevMode ? [{ debugName: "atMaxZoom" }] : /* istanbul ignore next */ []));
709
+ atFit = computed(() => this.viewportState().zoom <= 1 + 1e-4, /* @ts-ignore */
710
+ ...(ngDevMode ? [{ debugName: "atFit" }] : /* istanbul ignore next */ []));
549
711
  /** Placed geometry per section, keyed by section id. */
550
712
  layouts = computed(() => {
551
713
  const overrides = this.sectionOverrides();
@@ -634,6 +796,9 @@ class KerusiSeatmapComponent {
634
796
  }
635
797
  this.setFocusedSeat(seat.sectionId, seatId);
636
798
  this.seatFocus.emit(seat);
799
+ if (this.zoomable()) {
800
+ this.revealSeat(seatId);
801
+ }
637
802
  }
638
803
  /**
639
804
  * Arrow keys walk the navigation graph, which orders seats by `col` in grid
@@ -705,19 +870,247 @@ class KerusiSeatmapComponent {
705
870
  /**
706
871
  * Moves the roving tab stop, then the DOM focus that follows it.
707
872
  *
873
+ * When zoomable, the browser's own scroll-into-view is suppressed: the map
874
+ * moves by transform, so `onSeatFocused` pans it into view instead.
875
+ */
876
+ moveFocusTo(sectionId, seatId) {
877
+ this.setFocusedSeat(sectionId, seatId);
878
+ const node = this.seatNode(seatId);
879
+ if (this.zoomable()) {
880
+ node?.focus?.({ preventScroll: true });
881
+ }
882
+ else {
883
+ node?.focus?.();
884
+ }
885
+ }
886
+ /**
708
887
  * Seat ids come from the document, so they cannot be interpolated into a
709
888
  * selector unescaped — and `CSS.escape` is absent in jsdom and under SSR.
710
889
  * Scanning the rendered nodes avoids both problems.
711
890
  */
712
- moveFocusTo(sectionId, seatId) {
713
- this.setFocusedSeat(sectionId, seatId);
891
+ seatNode(seatId) {
714
892
  const nodes = this.host.nativeElement.querySelectorAll('[data-seat-id]');
715
893
  for (const node of Array.from(nodes)) {
716
894
  if (node.dataset['seatId'] === seatId) {
717
- node.focus?.();
895
+ return node;
896
+ }
897
+ }
898
+ return null;
899
+ }
900
+ // --- zoom and pan ---------------------------------------------------------
901
+ /** Zooms in one step about the centre. No-op unless `zoomable`. */
902
+ zoomIn() {
903
+ this.stepZoom(1);
904
+ }
905
+ /** Zooms out one step about the centre. No-op unless `zoomable`. */
906
+ zoomOut() {
907
+ this.stepZoom(-1);
908
+ }
909
+ /** Back to fit-to-container: the map's original view. No-op unless `zoomable`. */
910
+ resetZoom() {
911
+ const measured = this.measure();
912
+ if (measured && this.zoomable()) {
913
+ this.commit(fitViewport(measured.content, measured.container, this.limits()), true);
914
+ }
915
+ }
916
+ stepZoom(direction) {
917
+ const measured = this.measure();
918
+ if (measured && this.zoomable()) {
919
+ const next = stepViewport(this.viewportState(), direction, measured.content, measured.container, this.limits());
920
+ this.commit(next, true);
921
+ }
922
+ }
923
+ /** Sizes in CSS pixels, read when needed: `offset*` ignores the zoom transform. */
924
+ measure() {
925
+ const view = this.viewportEl()?.nativeElement;
926
+ const content = this.contentEl()?.nativeElement;
927
+ if (!view || !content) {
928
+ return null;
929
+ }
930
+ return {
931
+ container: { width: view.clientWidth, height: view.clientHeight },
932
+ content: { width: content.offsetWidth, height: content.offsetHeight },
933
+ };
934
+ }
935
+ commit(next, animate) {
936
+ if (viewportsEqual(next, this.viewportState())) {
937
+ return;
938
+ }
939
+ this.animated.set(animate);
940
+ this.viewportState.set(next);
941
+ this.viewportChange.emit(next);
942
+ }
943
+ reclamp() {
944
+ const measured = this.measure();
945
+ if (measured && this.zoomable()) {
946
+ const next = clampViewport(this.viewportState(), measured.content, measured.container, this.limits());
947
+ this.commit(next, false);
948
+ }
949
+ }
950
+ /** A point in the viewport's own coordinates, which is what the core functions take. */
951
+ containerPoint(event) {
952
+ const box = this.viewportEl().nativeElement.getBoundingClientRect();
953
+ return { x: event.clientX - box.left, y: event.clientY - box.top };
954
+ }
955
+ /**
956
+ * Pans a seat that took keyboard focus into view. A click or tap also
957
+ * focuses the seat, but should not move the map under the finger, so only a
958
+ * `:focus-visible` focus counts.
959
+ */
960
+ revealSeat(seatId) {
961
+ const view = this.viewportEl()?.nativeElement;
962
+ const node = this.seatNode(seatId);
963
+ const measured = this.measure();
964
+ if (!view || !node || !measured || !focusIsVisible(node)) {
965
+ return;
966
+ }
967
+ const seat = node.getBoundingClientRect();
968
+ const frame = view.getBoundingClientRect();
969
+ const next = revealRect(this.viewportState(), {
970
+ x: seat.left - frame.left,
971
+ y: seat.top - frame.top,
972
+ width: seat.width,
973
+ height: seat.height,
974
+ }, measured.content, measured.container, undefined, this.limits());
975
+ this.commit(next, true);
976
+ }
977
+ // --- pointer gestures -----------------------------------------------------
978
+ onPointerDown(event) {
979
+ if (event.pointerType === 'mouse' && event.button !== 0) {
980
+ return;
981
+ }
982
+ this.suppressClick = false;
983
+ this.feedGesture({ type: 'down', id: event.pointerId, point: this.containerPoint(event) });
984
+ }
985
+ onPointerMove(event) {
986
+ if (this.gesture.ids.length > 0) {
987
+ this.feedGesture({ type: 'move', id: event.pointerId, point: this.containerPoint(event) });
988
+ }
989
+ }
990
+ onPointerEnd(event, type) {
991
+ this.feedGesture({ type, id: event.pointerId });
992
+ }
993
+ feedGesture(event) {
994
+ const measured = this.measure();
995
+ if (!measured) {
996
+ return;
997
+ }
998
+ const result = reduceGesture(this.gesture, event, {
999
+ viewport: this.viewportState(),
1000
+ content: measured.content,
1001
+ container: measured.container,
1002
+ limits: this.limits(),
1003
+ });
1004
+ this.gesture = result.state;
1005
+ // Capture only once a tap has become a gesture: capturing on `down` would
1006
+ // retarget the tap's click away from the seat.
1007
+ if (result.capture) {
1008
+ const view = this.viewportEl()?.nativeElement;
1009
+ for (const id of result.state.ids) {
1010
+ try {
1011
+ view?.setPointerCapture(id);
1012
+ }
1013
+ catch {
1014
+ // The pointer already ended; nothing to capture.
1015
+ }
1016
+ }
1017
+ }
1018
+ if (result.viewport !== this.viewportState()) {
1019
+ this.commit(result.viewport, false);
1020
+ }
1021
+ if (result.suppressClick) {
1022
+ // The click a drag ends with arrives right after `pointerup`, before this timer.
1023
+ this.suppressClick = true;
1024
+ setTimeout(() => (this.suppressClick = false));
1025
+ }
1026
+ }
1027
+ onClickCapture(event) {
1028
+ if (this.suppressClick) {
1029
+ this.suppressClick = false;
1030
+ event.stopPropagation();
1031
+ event.preventDefault();
1032
+ }
1033
+ }
1034
+ onWheel(event) {
1035
+ const factor = wheelZoomFactor(event);
1036
+ const measured = this.measure();
1037
+ if (factor === null || !measured) {
1038
+ return; // A plain wheel is the page's to scroll.
1039
+ }
1040
+ event.preventDefault();
1041
+ const next = zoomViewportBy(this.viewportState(), factor, this.containerPoint(event), measured.content, measured.container, this.limits());
1042
+ this.commit(next, false);
1043
+ }
1044
+ onGestureStart(event) {
1045
+ event.preventDefault();
1046
+ this.gestureBase = this.viewportState();
1047
+ }
1048
+ onGestureChange(event) {
1049
+ event.preventDefault();
1050
+ const measured = this.measure();
1051
+ if (!this.gestureBase || !measured) {
1052
+ return;
1053
+ }
1054
+ const next = zoomViewportTo(this.gestureBase, this.gestureBase.zoom * event.scale, this.containerPoint(event), measured.content, measured.container, this.limits());
1055
+ this.commit(next, false);
1056
+ }
1057
+ onGestureEnd(event) {
1058
+ event.preventDefault();
1059
+ this.gestureBase = null;
1060
+ }
1061
+ // --- keyboard on the map as a whole ----------------------------------------
1062
+ /**
1063
+ * `+` `-` `0` zoom, and arrows pan when the viewport itself has focus; Esc
1064
+ * leaves a maximized map. A seat's own handler runs first, so an Esc that
1065
+ * cleared the selection is already `defaultPrevented` and stays at that.
1066
+ */
1067
+ onRootKeydown(event) {
1068
+ if (event.defaultPrevented || isTextEntryTarget(event.target)) {
1069
+ return;
1070
+ }
1071
+ if (this.zoomable()) {
1072
+ const measured = this.measure();
1073
+ const next = measured &&
1074
+ keyboardViewport(this.viewportState(), {
1075
+ key: event.key,
1076
+ ctrlKey: event.ctrlKey,
1077
+ metaKey: event.metaKey,
1078
+ altKey: event.altKey,
1079
+ viewportFocused: event.target === this.viewportEl()?.nativeElement,
1080
+ }, measured.content, measured.container, this.limits());
1081
+ if (next) {
1082
+ event.preventDefault();
1083
+ this.commit(next, true);
718
1084
  return;
719
1085
  }
720
1086
  }
1087
+ if (event.key === 'Escape' && this.maximizedState()) {
1088
+ event.preventDefault();
1089
+ this.setMaximized(false);
1090
+ }
1091
+ }
1092
+ // --- maximize -------------------------------------------------------------
1093
+ /** Maximizes or restores the map. Maximizing needs `maximizable`. */
1094
+ toggleMaximized() {
1095
+ this.setMaximized(!this.maximizedState());
1096
+ }
1097
+ setMaximized(next) {
1098
+ if (next === this.maximizedState() || (next && !this.maximizable())) {
1099
+ return;
1100
+ }
1101
+ this.maximizedState.set(next);
1102
+ this.maximizedChange.emit(next);
1103
+ // Native full screen is an enhancement over the fixed-position layout, which
1104
+ // is all iPhone Safari offers. It is requested here, inside the click that
1105
+ // got us here, because browsers only grant it to a user gesture.
1106
+ const host = this.host.nativeElement;
1107
+ const doc = host.ownerDocument;
1108
+ if (next && !doc.fullscreenElement) {
1109
+ host.requestFullscreen?.()?.catch(() => undefined);
1110
+ }
1111
+ else if (!next && doc.fullscreenElement === host) {
1112
+ doc.exitFullscreen?.()?.catch(() => undefined);
1113
+ }
721
1114
  }
722
1115
  announceSelection(selection) {
723
1116
  const strings = this.ariaStrings();
@@ -749,15 +1142,24 @@ class KerusiSeatmapComponent {
749
1142
  return (this.map().sections ?? []).map((s) => s.id).filter((id) => !hidden.includes(id));
750
1143
  }
751
1144
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSeatmapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
752
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiSeatmapComponent, isStandalone: true, selector: "kerusi-seatmap", inputs: { map: { classPropertyName: "map", publicName: "map", isSignal: true, isRequired: true, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, session: { classPropertyName: "session", publicName: "session", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, rtl: { classPropertyName: "rtl", publicName: "rtl", isSignal: true, isRequired: false, transformFunction: null }, ariaStrings: { classPropertyName: "ariaStrings", publicName: "ariaStrings", isSignal: true, isRequired: false, transformFunction: null }, colors: { classPropertyName: "colors", publicName: "colors", isSignal: true, isRequired: false, transformFunction: null }, typeColors: { classPropertyName: "typeColors", publicName: "typeColors", isSignal: true, isRequired: false, transformFunction: null }, seatSize: { classPropertyName: "seatSize", publicName: "seatSize", isSignal: true, isRequired: false, transformFunction: null }, seatGap: { classPropertyName: "seatGap", publicName: "seatGap", isSignal: true, isRequired: false, transformFunction: null }, freeformBasis: { classPropertyName: "freeformBasis", publicName: "freeformBasis", isSignal: true, isRequired: false, transformFunction: null }, unitScale: { classPropertyName: "unitScale", publicName: "unitScale", isSignal: true, isRequired: false, transformFunction: null }, showSectionLabels: { classPropertyName: "showSectionLabels", publicName: "showSectionLabels", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showLegendPrices: { classPropertyName: "showLegendPrices", publicName: "showLegendPrices", isSignal: true, isRequired: false, transformFunction: null }, sectionIds: { classPropertyName: "sectionIds", publicName: "sectionIds", isSignal: true, isRequired: false, transformFunction: null }, sectionOverrides: { classPropertyName: "sectionOverrides", publicName: "sectionOverrides", isSignal: true, isRequired: false, transformFunction: null }, selectableStatuses: { classPropertyName: "selectableStatuses", publicName: "selectableStatuses", isSignal: true, isRequired: false, transformFunction: null }, companionMode: { classPropertyName: "companionMode", publicName: "companionMode", isSignal: true, isRequired: false, transformFunction: null }, maxSelection: { classPropertyName: "maxSelection", publicName: "maxSelection", isSignal: true, isRequired: false, transformFunction: null }, seatSelectable: { classPropertyName: "seatSelectable", publicName: "seatSelectable", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, expireHolds: { classPropertyName: "expireHolds", publicName: "expireHolds", isSignal: true, isRequired: false, transformFunction: null }, expiryIntervalMs: { classPropertyName: "expiryIntervalMs", publicName: "expiryIntervalMs", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selection: "selectionChange", seatSelect: "seatSelect", seatDeselect: "seatDeselect", seatDisallowed: "seatDisallowed", seatFocus: "seatFocus", validationIssues: "validationIssues" }, ngImport: i0, template: "<div\r\n class=\"kerusi-seatmap\"\r\n role=\"group\"\r\n [attr.aria-label]=\"mapLabel()\"\r\n [attr.dir]=\"isRtl() ? 'rtl' : null\"\r\n [attr.lang]=\"resolvedLocale()\"\r\n>\r\n @for (section of renderMap().sections; track section.id) {\r\n <section\r\n class=\"kerusi-seatmap__section kerusi-seatmap__section--{{ section.layoutMode }}\"\r\n role=\"group\"\r\n [attr.aria-labelledby]=\"labelFor(section) ? labelId(section) : null\"\r\n [attr.aria-label]=\"labelFor(section) ? null : section.id\"\r\n >\r\n @if (showSectionLabels() && labelFor(section)) {\r\n <h3 class=\"kerusi-section-label\" [id]=\"labelId(section)\">{{ labelFor(section) }}</h3>\r\n }\r\n\r\n <kerusi-section\r\n [section]=\"section\"\r\n [layout]=\"layoutFor(section)\"\r\n [selection]=\"selectionSet()\"\r\n [focusedSeatId]=\"focusedSeatIn(section)\"\r\n [ariaLabels]=\"ariaLabels()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [unitScale]=\"unitScale()\"\r\n [interactive]=\"interactive()\"\r\n (seatActivate)=\"onSeatActivate($event)\"\r\n (seatFocused)=\"onSeatFocused($event)\"\r\n (seatKeydown)=\"onKeydown(section, $event)\"\r\n />\r\n </section>\r\n }\r\n\r\n @if (showLegend()) {\r\n <kerusi-legend\r\n [legend]=\"renderMap().legend\"\r\n [locale]=\"resolvedLocale()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [showPrices]=\"showLegendPrices()\"\r\n />\r\n }\r\n\r\n <p class=\"kerusi-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveMessage() }}</p>\r\n</div>\r\n", styles: [":host{display:block}.kerusi-seatmap{display:flex;flex-direction:column;gap:1.25rem}.kerusi-seatmap__section{display:flex;flex-direction:column;gap:.4rem}.kerusi-section-label{margin:0;font-size:.85rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.75}.kerusi-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: KerusiSectionComponent, selector: "kerusi-section", inputs: ["section", "layout", "selection", "focusedSeatId", "ariaLabels", "colors", "typeColors", "interactive", "unitScale"], outputs: ["seatActivate", "seatFocused", "seatKeydown"] }, { kind: "component", type: KerusiLegendComponent, selector: "kerusi-legend", inputs: ["legend", "locale", "colors", "typeColors", "showPrices", "hideUnusedTypes", "headingSeatTypes", "headingAvailability"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1145
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiSeatmapComponent, isStandalone: true, selector: "kerusi-seatmap", inputs: { map: { classPropertyName: "map", publicName: "map", isSignal: true, isRequired: true, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, session: { classPropertyName: "session", publicName: "session", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, rtl: { classPropertyName: "rtl", publicName: "rtl", isSignal: true, isRequired: false, transformFunction: null }, ariaStrings: { classPropertyName: "ariaStrings", publicName: "ariaStrings", isSignal: true, isRequired: false, transformFunction: null }, colors: { classPropertyName: "colors", publicName: "colors", isSignal: true, isRequired: false, transformFunction: null }, typeColors: { classPropertyName: "typeColors", publicName: "typeColors", isSignal: true, isRequired: false, transformFunction: null }, seatSize: { classPropertyName: "seatSize", publicName: "seatSize", isSignal: true, isRequired: false, transformFunction: null }, seatGap: { classPropertyName: "seatGap", publicName: "seatGap", isSignal: true, isRequired: false, transformFunction: null }, freeformBasis: { classPropertyName: "freeformBasis", publicName: "freeformBasis", isSignal: true, isRequired: false, transformFunction: null }, unitScale: { classPropertyName: "unitScale", publicName: "unitScale", isSignal: true, isRequired: false, transformFunction: null }, showSectionLabels: { classPropertyName: "showSectionLabels", publicName: "showSectionLabels", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showLegendPrices: { classPropertyName: "showLegendPrices", publicName: "showLegendPrices", isSignal: true, isRequired: false, transformFunction: null }, sectionIds: { classPropertyName: "sectionIds", publicName: "sectionIds", isSignal: true, isRequired: false, transformFunction: null }, sectionOverrides: { classPropertyName: "sectionOverrides", publicName: "sectionOverrides", isSignal: true, isRequired: false, transformFunction: null }, selectableStatuses: { classPropertyName: "selectableStatuses", publicName: "selectableStatuses", isSignal: true, isRequired: false, transformFunction: null }, companionMode: { classPropertyName: "companionMode", publicName: "companionMode", isSignal: true, isRequired: false, transformFunction: null }, maxSelection: { classPropertyName: "maxSelection", publicName: "maxSelection", isSignal: true, isRequired: false, transformFunction: null }, seatSelectable: { classPropertyName: "seatSelectable", publicName: "seatSelectable", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, expireHolds: { classPropertyName: "expireHolds", publicName: "expireHolds", isSignal: true, isRequired: false, transformFunction: null }, expiryIntervalMs: { classPropertyName: "expiryIntervalMs", publicName: "expiryIntervalMs", isSignal: true, isRequired: false, transformFunction: null }, zoomable: { classPropertyName: "zoomable", publicName: "zoomable", isSignal: true, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: true, isRequired: false, transformFunction: null }, showZoomControls: { classPropertyName: "showZoomControls", publicName: "showZoomControls", isSignal: true, isRequired: false, transformFunction: null }, maximizable: { classPropertyName: "maximizable", publicName: "maximizable", isSignal: true, isRequired: false, transformFunction: null }, controlStrings: { classPropertyName: "controlStrings", publicName: "controlStrings", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selection: "selectionChange", seatSelect: "seatSelect", seatDeselect: "seatDeselect", seatDisallowed: "seatDisallowed", seatFocus: "seatFocus", validationIssues: "validationIssues", viewportChange: "viewportChange", maximizedChange: "maximizedChange" }, viewQueries: [{ propertyName: "viewportEl", first: true, predicate: ["viewport"], descendants: true, isSignal: true }, { propertyName: "contentEl", first: true, predicate: ["content"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\r\n class=\"kerusi-seatmap\"\r\n [class.kerusi-seatmap--maximized]=\"maximized()\"\r\n role=\"group\"\r\n [attr.aria-label]=\"mapLabel()\"\r\n [attr.dir]=\"isRtl() ? 'rtl' : null\"\r\n [attr.lang]=\"resolvedLocale()\"\r\n [style.background]=\"maximized() ? controlColors().maximizedBackground : null\"\r\n [style.color]=\"maximized() ? controlColors().maximizedColor : null\"\r\n (keydown)=\"onRootKeydown($event)\"\r\n>\r\n @if (zoomable() || maximizable()) {\r\n <!-- Only when a zoom or maximize feature is on, so a plain map's markup is\r\n exactly what it was before they existed. -->\r\n <div class=\"kerusi-seatmap__stage\">\r\n @if (showToolbar()) {\r\n <div\r\n class=\"kerusi-toolbar\"\r\n role=\"toolbar\"\r\n [attr.aria-label]=\"strings().toolbar\"\r\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\r\n >\r\n @if (zoomable() && showZoomControls()) {\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-in\"\r\n [attr.aria-label]=\"strings().zoomIn\"\r\n [attr.aria-disabled]=\"atMaxZoom()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"zoomIn()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M12 5v14M5 12h14\" />\r\n </svg>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-out\"\r\n [attr.aria-label]=\"strings().zoomOut\"\r\n [attr.aria-disabled]=\"atFit()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"zoomOut()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M5 12h14\" />\r\n </svg>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-reset\"\r\n [attr.aria-label]=\"strings().zoomReset\"\r\n [attr.aria-disabled]=\"atFit()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"resetZoom()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M3 12a9 9 0 1 0 3-6.7M3 4v5h5\" />\r\n </svg>\r\n </button>\r\n }\r\n @if (maximizable()) {\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--maximize\"\r\n [attr.aria-label]=\"maximized() ? strings().restore : strings().maximize\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"toggleMaximized()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n @if (maximized()) {\r\n <path d=\"M9 4v5H4M15 4v5h5M15 20v-5h5M9 20v-5H4\" />\r\n } @else {\r\n <path d=\"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5\" />\r\n }\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (zoomable()) {\r\n <div\r\n #viewport\r\n class=\"kerusi-viewport\"\r\n role=\"group\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"strings().viewport\"\r\n [style.touch-action]=\"touchAction()\"\r\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\r\n >\r\n <div\r\n #content\r\n class=\"kerusi-viewport__content\"\r\n [class.kerusi-viewport__content--animated]=\"animated()\"\r\n [style.transform]=\"transform()\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n </div>\r\n </div>\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n }\r\n </div>\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n }\r\n\r\n @if (showLegend()) {\r\n <kerusi-legend\r\n [legend]=\"renderMap().legend\"\r\n [locale]=\"resolvedLocale()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [showPrices]=\"showLegendPrices()\"\r\n />\r\n }\r\n\r\n <p class=\"kerusi-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveMessage() }}</p>\r\n</div>\r\n\r\n<ng-template #sections>\r\n @for (section of renderMap().sections; track section.id) {\r\n <section\r\n class=\"kerusi-seatmap__section kerusi-seatmap__section--{{ section.layoutMode }}\"\r\n role=\"group\"\r\n [attr.aria-labelledby]=\"labelFor(section) ? labelId(section) : null\"\r\n [attr.aria-label]=\"labelFor(section) ? null : section.id\"\r\n >\r\n @if (showSectionLabels() && labelFor(section)) {\r\n <h3 class=\"kerusi-section-label\" [id]=\"labelId(section)\">{{ labelFor(section) }}</h3>\r\n }\r\n\r\n <kerusi-section\r\n [section]=\"section\"\r\n [layout]=\"layoutFor(section)\"\r\n [selection]=\"selectionSet()\"\r\n [focusedSeatId]=\"focusedSeatIn(section)\"\r\n [ariaLabels]=\"ariaLabels()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [unitScale]=\"unitScale()\"\r\n [interactive]=\"interactive()\"\r\n (seatActivate)=\"onSeatActivate($event)\"\r\n (seatFocused)=\"onSeatFocused($event)\"\r\n (seatKeydown)=\"onKeydown(section, $event)\"\r\n />\r\n </section>\r\n }\r\n</ng-template>\r\n", styles: [":host{display:block}.kerusi-seatmap{display:flex;flex-direction:column;gap:1.25rem}.kerusi-seatmap__section{display:flex;flex-direction:column;gap:.4rem}.kerusi-section-label{margin:0;font-size:.85rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.75}.kerusi-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}.kerusi-seatmap__stage{display:flex;flex-direction:column;gap:1.25rem}.kerusi-toolbar{display:flex;gap:.35rem;align-self:flex-end;flex:none}.kerusi-control{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:2.75rem;height:2.75rem;padding:0;border:1px solid;border-radius:.4rem;font:inherit;cursor:pointer}.kerusi-control[aria-disabled=true]{opacity:.45;cursor:default}.kerusi-control svg{width:1.4rem;height:1.4rem;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}.kerusi-control:focus-visible,.kerusi-viewport:focus-visible{outline:2px solid var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));outline-offset:2px}.kerusi-viewport{position:relative;overflow:hidden;overflow:clip;user-select:none;-webkit-user-select:none}.kerusi-viewport__content{display:flex;flex-direction:column;gap:1.25rem;transform-origin:0 0}.kerusi-viewport__content--animated{transition:transform .16s ease-out}@media(prefers-reduced-motion:reduce){.kerusi-viewport__content--animated{transition:none}}.kerusi-seatmap--maximized{position:fixed;inset:0;z-index:var(--kerusi-maximized-z-index, 1000);box-sizing:border-box;padding:.75rem;gap:.5rem;overflow:hidden;overscroll-behavior:contain}.kerusi-seatmap--maximized .kerusi-seatmap__stage{flex:1 1 auto;min-height:0;gap:.5rem;overflow:auto}.kerusi-seatmap--maximized .kerusi-viewport{flex:1 1 auto;min-height:0}\n"], dependencies: [{ kind: "component", type: KerusiSectionComponent, selector: "kerusi-section", inputs: ["section", "layout", "selection", "focusedSeatId", "ariaLabels", "colors", "typeColors", "interactive", "unitScale"], outputs: ["seatActivate", "seatFocused", "seatKeydown"] }, { kind: "component", type: KerusiLegendComponent, selector: "kerusi-legend", inputs: ["legend", "locale", "colors", "typeColors", "showPrices", "hideUnusedTypes", "headingSeatTypes", "headingAvailability"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
753
1146
  }
754
1147
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSeatmapComponent, decorators: [{
755
1148
  type: Component,
756
- args: [{ selector: 'kerusi-seatmap', changeDetection: ChangeDetectionStrategy.OnPush, imports: [KerusiSectionComponent, KerusiLegendComponent], template: "<div\r\n class=\"kerusi-seatmap\"\r\n role=\"group\"\r\n [attr.aria-label]=\"mapLabel()\"\r\n [attr.dir]=\"isRtl() ? 'rtl' : null\"\r\n [attr.lang]=\"resolvedLocale()\"\r\n>\r\n @for (section of renderMap().sections; track section.id) {\r\n <section\r\n class=\"kerusi-seatmap__section kerusi-seatmap__section--{{ section.layoutMode }}\"\r\n role=\"group\"\r\n [attr.aria-labelledby]=\"labelFor(section) ? labelId(section) : null\"\r\n [attr.aria-label]=\"labelFor(section) ? null : section.id\"\r\n >\r\n @if (showSectionLabels() && labelFor(section)) {\r\n <h3 class=\"kerusi-section-label\" [id]=\"labelId(section)\">{{ labelFor(section) }}</h3>\r\n }\r\n\r\n <kerusi-section\r\n [section]=\"section\"\r\n [layout]=\"layoutFor(section)\"\r\n [selection]=\"selectionSet()\"\r\n [focusedSeatId]=\"focusedSeatIn(section)\"\r\n [ariaLabels]=\"ariaLabels()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [unitScale]=\"unitScale()\"\r\n [interactive]=\"interactive()\"\r\n (seatActivate)=\"onSeatActivate($event)\"\r\n (seatFocused)=\"onSeatFocused($event)\"\r\n (seatKeydown)=\"onKeydown(section, $event)\"\r\n />\r\n </section>\r\n }\r\n\r\n @if (showLegend()) {\r\n <kerusi-legend\r\n [legend]=\"renderMap().legend\"\r\n [locale]=\"resolvedLocale()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [showPrices]=\"showLegendPrices()\"\r\n />\r\n }\r\n\r\n <p class=\"kerusi-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveMessage() }}</p>\r\n</div>\r\n", styles: [":host{display:block}.kerusi-seatmap{display:flex;flex-direction:column;gap:1.25rem}.kerusi-seatmap__section{display:flex;flex-direction:column;gap:.4rem}.kerusi-section-label{margin:0;font-size:.85rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.75}.kerusi-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}\n"] }]
757
- }], ctorParameters: () => [], propDecorators: { map: [{ type: i0.Input, args: [{ isSignal: true, alias: "map", required: true }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], session: [{ type: i0.Input, args: [{ isSignal: true, alias: "session", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], rtl: [{ type: i0.Input, args: [{ isSignal: true, alias: "rtl", required: false }] }], ariaStrings: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaStrings", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], seatSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatSize", required: false }] }], seatGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatGap", required: false }] }], freeformBasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "freeformBasis", required: false }] }], unitScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "unitScale", required: false }] }], showSectionLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSectionLabels", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], showLegendPrices: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegendPrices", required: false }] }], sectionIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionIds", required: false }] }], sectionOverrides: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionOverrides", required: false }] }], selectableStatuses: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectableStatuses", required: false }] }], companionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "companionMode", required: false }] }], maxSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelection", required: false }] }], seatSelectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatSelectable", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], expireHolds: [{ type: i0.Input, args: [{ isSignal: true, alias: "expireHolds", required: false }] }], expiryIntervalMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "expiryIntervalMs", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], seatSelect: [{ type: i0.Output, args: ["seatSelect"] }], seatDeselect: [{ type: i0.Output, args: ["seatDeselect"] }], seatDisallowed: [{ type: i0.Output, args: ["seatDisallowed"] }], seatFocus: [{ type: i0.Output, args: ["seatFocus"] }], validationIssues: [{ type: i0.Output, args: ["validationIssues"] }] } });
1149
+ args: [{ selector: 'kerusi-seatmap', changeDetection: ChangeDetectionStrategy.OnPush, imports: [KerusiSectionComponent, KerusiLegendComponent, NgTemplateOutlet], template: "<div\r\n class=\"kerusi-seatmap\"\r\n [class.kerusi-seatmap--maximized]=\"maximized()\"\r\n role=\"group\"\r\n [attr.aria-label]=\"mapLabel()\"\r\n [attr.dir]=\"isRtl() ? 'rtl' : null\"\r\n [attr.lang]=\"resolvedLocale()\"\r\n [style.background]=\"maximized() ? controlColors().maximizedBackground : null\"\r\n [style.color]=\"maximized() ? controlColors().maximizedColor : null\"\r\n (keydown)=\"onRootKeydown($event)\"\r\n>\r\n @if (zoomable() || maximizable()) {\r\n <!-- Only when a zoom or maximize feature is on, so a plain map's markup is\r\n exactly what it was before they existed. -->\r\n <div class=\"kerusi-seatmap__stage\">\r\n @if (showToolbar()) {\r\n <div\r\n class=\"kerusi-toolbar\"\r\n role=\"toolbar\"\r\n [attr.aria-label]=\"strings().toolbar\"\r\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\r\n >\r\n @if (zoomable() && showZoomControls()) {\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-in\"\r\n [attr.aria-label]=\"strings().zoomIn\"\r\n [attr.aria-disabled]=\"atMaxZoom()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"zoomIn()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M12 5v14M5 12h14\" />\r\n </svg>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-out\"\r\n [attr.aria-label]=\"strings().zoomOut\"\r\n [attr.aria-disabled]=\"atFit()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"zoomOut()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M5 12h14\" />\r\n </svg>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-reset\"\r\n [attr.aria-label]=\"strings().zoomReset\"\r\n [attr.aria-disabled]=\"atFit()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"resetZoom()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M3 12a9 9 0 1 0 3-6.7M3 4v5h5\" />\r\n </svg>\r\n </button>\r\n }\r\n @if (maximizable()) {\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--maximize\"\r\n [attr.aria-label]=\"maximized() ? strings().restore : strings().maximize\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"toggleMaximized()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n @if (maximized()) {\r\n <path d=\"M9 4v5H4M15 4v5h5M15 20v-5h5M9 20v-5H4\" />\r\n } @else {\r\n <path d=\"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5\" />\r\n }\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (zoomable()) {\r\n <div\r\n #viewport\r\n class=\"kerusi-viewport\"\r\n role=\"group\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"strings().viewport\"\r\n [style.touch-action]=\"touchAction()\"\r\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\r\n >\r\n <div\r\n #content\r\n class=\"kerusi-viewport__content\"\r\n [class.kerusi-viewport__content--animated]=\"animated()\"\r\n [style.transform]=\"transform()\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n </div>\r\n </div>\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n }\r\n </div>\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n }\r\n\r\n @if (showLegend()) {\r\n <kerusi-legend\r\n [legend]=\"renderMap().legend\"\r\n [locale]=\"resolvedLocale()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [showPrices]=\"showLegendPrices()\"\r\n />\r\n }\r\n\r\n <p class=\"kerusi-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveMessage() }}</p>\r\n</div>\r\n\r\n<ng-template #sections>\r\n @for (section of renderMap().sections; track section.id) {\r\n <section\r\n class=\"kerusi-seatmap__section kerusi-seatmap__section--{{ section.layoutMode }}\"\r\n role=\"group\"\r\n [attr.aria-labelledby]=\"labelFor(section) ? labelId(section) : null\"\r\n [attr.aria-label]=\"labelFor(section) ? null : section.id\"\r\n >\r\n @if (showSectionLabels() && labelFor(section)) {\r\n <h3 class=\"kerusi-section-label\" [id]=\"labelId(section)\">{{ labelFor(section) }}</h3>\r\n }\r\n\r\n <kerusi-section\r\n [section]=\"section\"\r\n [layout]=\"layoutFor(section)\"\r\n [selection]=\"selectionSet()\"\r\n [focusedSeatId]=\"focusedSeatIn(section)\"\r\n [ariaLabels]=\"ariaLabels()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [unitScale]=\"unitScale()\"\r\n [interactive]=\"interactive()\"\r\n (seatActivate)=\"onSeatActivate($event)\"\r\n (seatFocused)=\"onSeatFocused($event)\"\r\n (seatKeydown)=\"onKeydown(section, $event)\"\r\n />\r\n </section>\r\n }\r\n</ng-template>\r\n", styles: [":host{display:block}.kerusi-seatmap{display:flex;flex-direction:column;gap:1.25rem}.kerusi-seatmap__section{display:flex;flex-direction:column;gap:.4rem}.kerusi-section-label{margin:0;font-size:.85rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.75}.kerusi-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}.kerusi-seatmap__stage{display:flex;flex-direction:column;gap:1.25rem}.kerusi-toolbar{display:flex;gap:.35rem;align-self:flex-end;flex:none}.kerusi-control{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:2.75rem;height:2.75rem;padding:0;border:1px solid;border-radius:.4rem;font:inherit;cursor:pointer}.kerusi-control[aria-disabled=true]{opacity:.45;cursor:default}.kerusi-control svg{width:1.4rem;height:1.4rem;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}.kerusi-control:focus-visible,.kerusi-viewport:focus-visible{outline:2px solid var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));outline-offset:2px}.kerusi-viewport{position:relative;overflow:hidden;overflow:clip;user-select:none;-webkit-user-select:none}.kerusi-viewport__content{display:flex;flex-direction:column;gap:1.25rem;transform-origin:0 0}.kerusi-viewport__content--animated{transition:transform .16s ease-out}@media(prefers-reduced-motion:reduce){.kerusi-viewport__content--animated{transition:none}}.kerusi-seatmap--maximized{position:fixed;inset:0;z-index:var(--kerusi-maximized-z-index, 1000);box-sizing:border-box;padding:.75rem;gap:.5rem;overflow:hidden;overscroll-behavior:contain}.kerusi-seatmap--maximized .kerusi-seatmap__stage{flex:1 1 auto;min-height:0;gap:.5rem;overflow:auto}.kerusi-seatmap--maximized .kerusi-viewport{flex:1 1 auto;min-height:0}\n"] }]
1150
+ }], ctorParameters: () => [], propDecorators: { map: [{ type: i0.Input, args: [{ isSignal: true, alias: "map", required: true }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], session: [{ type: i0.Input, args: [{ isSignal: true, alias: "session", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], rtl: [{ type: i0.Input, args: [{ isSignal: true, alias: "rtl", required: false }] }], ariaStrings: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaStrings", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], seatSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatSize", required: false }] }], seatGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatGap", required: false }] }], freeformBasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "freeformBasis", required: false }] }], unitScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "unitScale", required: false }] }], showSectionLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSectionLabels", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], showLegendPrices: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegendPrices", required: false }] }], sectionIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionIds", required: false }] }], sectionOverrides: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionOverrides", required: false }] }], selectableStatuses: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectableStatuses", required: false }] }], companionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "companionMode", required: false }] }], maxSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelection", required: false }] }], seatSelectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatSelectable", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], expireHolds: [{ type: i0.Input, args: [{ isSignal: true, alias: "expireHolds", required: false }] }], expiryIntervalMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "expiryIntervalMs", required: false }] }], zoomable: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomable", required: false }] }], maxZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxZoom", required: false }] }], showZoomControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "showZoomControls", required: false }] }], maximizable: [{ type: i0.Input, args: [{ isSignal: true, alias: "maximizable", required: false }] }], controlStrings: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlStrings", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], seatSelect: [{ type: i0.Output, args: ["seatSelect"] }], seatDeselect: [{ type: i0.Output, args: ["seatDeselect"] }], seatDisallowed: [{ type: i0.Output, args: ["seatDisallowed"] }], seatFocus: [{ type: i0.Output, args: ["seatFocus"] }], validationIssues: [{ type: i0.Output, args: ["validationIssues"] }], viewportChange: [{ type: i0.Output, args: ["viewportChange"] }], maximizedChange: [{ type: i0.Output, args: ["maximizedChange"] }], viewportEl: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }], contentEl: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }] } });
758
1151
  function formatTotal(total, locale) {
759
1152
  return total ? formatMoney(total, locale) : '';
760
1153
  }
1154
+ /** `:focus-visible` where the engine knows it; otherwise assume the focus is the keyboard's. */
1155
+ function focusIsVisible(node) {
1156
+ try {
1157
+ return node.matches(':focus-visible');
1158
+ }
1159
+ catch {
1160
+ return true;
1161
+ }
1162
+ }
761
1163
 
762
1164
  /**
763
1165
  * The Kerusi-native seat map: `<kerusi-seatmap>`, its section renderer, and an