@kerusiweb/angular 2.3.0 → 3.0.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,24 +1,34 @@
1
1
  import { NgTemplateOutlet } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
3
  import { input, output, computed, ChangeDetectionStrategy, Component, inject, TemplateRef, Directive, ElementRef, model, contentChild, signal, viewChild, effect, untracked } from '@angular/core';
4
- import { DEFAULT_KERUSI_COLORS, resolveSeatStyle, resolveSeatGlyph, cssVar, elementStyle, elementFill, elementTextFill, screenPath, legendSwatchFill, legendSwatchPath, legendSwatchText, seatBodyPath, seatSelectedFrame, seatOccupantPath, seatOccupantStroke, seatWashFill, occupantFillFor, occupantStrokeFor, seatCoreFill, 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
+ import { DEFAULT_KERUSI_COLORS, elementTextFill, elementStyle, resolveSeatStyle, resolveSeatGlyph, elementFill, screenPath, cssVar, legendSwatchFill, legendSwatchPath, legendSwatchText, seatBodyPath, seatSelectedFrame, seatOccupantPath, seatOccupantStroke, seatWashFill, occupantFillFor, occupantStrokeFor, seatCoreFill, 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, computeMapLayout, buildNavigationGraph, seatAriaLabel, summarizeSelection, toggleSeatSelection, disallowedAnnouncement, fitViewport, viewportForRect, canvasRectToContent, stepViewport, clampViewport, revealRect, reduceGesture, wheelZoomFactor, zoomViewportBy, zoomViewportTo, isTextEntryTarget, keyboardViewport, DEFAULT_SEQUENCE_READER, heldSeats, applyStateDeltaOrdered } from '@kerusiweb/core';
5
5
 
6
6
  /**
7
- * Draws one Kerusi `Section` as a single `<svg>`.
7
+ * Draws one Kerusi `Section` as a `<g>` on the map's shared canvas (D1).
8
8
  *
9
- * One SVG per section — rather than section groups inside a shared canvas — is
10
- * what makes per-section `aspectRatio` and mixed grid/freeform modes work: each
11
- * gets its own `viewBox` and its own intrinsic proportions, and CSS is free to
12
- * stack, wrap or column them. It also puts the section heading in real DOM,
13
- * where a screen reader can use it as a landmark.
9
+ * Sections are zones on one canvas in rev 15 (§4.1), and two may sit side by
10
+ * side, so a section is a group inside the map's single `<svg>` rather than an
11
+ * SVG of its own. Use it on a `<g>` inside an `<svg>` whose `viewBox` is the
12
+ * canvas: `<g kerusi-section [layout]="placed"></g>`. Its template prefixes
13
+ * every element with `svg:`, because Angular cannot tell from a component's
14
+ * own template that it will be stamped into SVG.
14
15
  *
15
16
  * This component holds no state. It takes a placed layout and emits intent.
16
17
  */
17
18
  class KerusiSectionComponent {
18
- section = input.required(/* @ts-ignore */
19
- ...(ngDevMode ? [{ debugName: "section" }] : /* istanbul ignore next */ []));
19
+ /** The section with its seats and elements placed on the map's canvas. */
20
20
  layout = input.required(/* @ts-ignore */
21
21
  ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
22
+ /** The section's name: its accessible name, and its drawn label when one is shown. */
23
+ label = input(undefined, /* @ts-ignore */
24
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
25
+ /**
26
+ * Draw `label` inside the SVG, at the top-left of the section's bounds. Only
27
+ * a section that declares `bounds` gets one (D5); otherwise the label is the
28
+ * group's accessible name alone.
29
+ */
30
+ showLabel = input(true, /* @ts-ignore */
31
+ ...(ngDevMode ? [{ debugName: "showLabel" }] : /* istanbul ignore next */ []));
22
32
  /** Currently selected seat ids. */
23
33
  selection = input(new Set(), /* @ts-ignore */
24
34
  ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
@@ -45,25 +55,14 @@ class KerusiSectionComponent {
45
55
  /** When false, seats render but do not respond to pointer or keyboard. */
46
56
  interactive = input(true, /* @ts-ignore */
47
57
  ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
48
- /**
49
- * CSS pixels per viewBox unit, at the section's natural size.
50
- *
51
- * Without a cap every section stretches to the full container width, so in a
52
- * four-tier theatre the narrow box row would draw seats several times the
53
- * size of the orchestra's. Capping each section at its own intrinsic width
54
- * keeps a seat the same size everywhere; a section too wide to fit still
55
- * scales down to the container.
56
- */
57
- unitScale = input(1, /* @ts-ignore */
58
- ...(ngDevMode ? [{ debugName: "unitScale" }] : /* istanbul ignore next */ []));
59
58
  seatActivate = output();
60
59
  seatFocused = output();
61
60
  seatKeydown = output();
62
- viewBox = computed(() => `0 0 ${this.layout().width} ${this.layout().height}`, /* @ts-ignore */
63
- ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
64
- /** The section's natural width in CSS pixels; it never draws wider. */
65
- naturalWidth = computed(() => this.layout().width * this.unitScale(), /* @ts-ignore */
66
- ...(ngDevMode ? [{ debugName: "naturalWidth" }] : /* istanbul ignore next */ []));
61
+ /** Whether the label is drawn: asked for, present, and with declared bounds to sit in. */
62
+ drawsLabel = computed(() => this.showLabel() && !!this.label() && this.layout().section.boundsDeclared, /* @ts-ignore */
63
+ ...(ngDevMode ? [{ debugName: "drawsLabel" }] : /* istanbul ignore next */ []));
64
+ labelFill = computed(() => elementTextFill(elementStyle('label').tone, this.colors()), /* @ts-ignore */
65
+ ...(ngDevMode ? [{ debugName: "labelFill" }] : /* istanbul ignore next */ []));
67
66
  /**
68
67
  * Every seat with its glyph resolved, once per change of layout, selection or
69
68
  * theme rather than once per binding per change detection.
@@ -85,9 +84,6 @@ class KerusiSectionComponent {
85
84
  });
86
85
  }, /* @ts-ignore */
87
86
  ...(ngDevMode ? [{ debugName: "seats" }] : /* istanbul ignore next */ []));
88
- backdrop() {
89
- return cssVar('backdrop', this.colors().backdrop);
90
- }
91
87
  /** The section's single tab stop; every other seat is reachable by arrow key. */
92
88
  tabIndex(placed) {
93
89
  if (!this.interactive()) {
@@ -149,12 +145,17 @@ class KerusiSectionComponent {
149
145
  }
150
146
  }
151
147
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
152
- 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 }, seatTypeStyles: { classPropertyName: "seatTypeStyles", publicName: "seatTypeStyles", isSignal: true, isRequired: false, transformFunction: null }, seatStyle: { classPropertyName: "seatStyle", publicName: "seatStyle", isSignal: true, isRequired: false, transformFunction: null }, seatTemplate: { classPropertyName: "seatTemplate", publicName: "seatTemplate", 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 <!-- The stock seat drawing. A template rather than inline markup so that a\r\n custom seat template can render it too, as `defaultContent`. Declared\r\n inside the <svg>, so its elements are created in the SVG namespace. -->\r\n <ng-template #stockSeat let-seat>\r\n @let glyph = seat.glyph;\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]=\"glyph.body.d\" [style.fill]=\"glyph.body.fill\" />\r\n @if (glyph.wash; as wash) {\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 class=\"kerusi-seat__wash\" [attr.d]=\"wash.d\" [style.fill]=\"wash.fill\" />\r\n }\r\n @if (glyph.core; as core) {\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]=\"core.d\" [style.fill]=\"core.fill\" />\r\n }\r\n @if (glyph.occupant; as occupant) {\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--{{ occupant.variant }}\"\r\n [attr.d]=\"occupant.d\"\r\n [attr.stroke-width]=\"occupant.strokeWidth\"\r\n [style.fill]=\"occupant.fill\"\r\n [style.stroke]=\"occupant.stroke\"\r\n />\r\n }\r\n <!-- The halo colour and width are bound here rather than in the\r\n stylesheet: a CSS `var(--kerusi-selected-fg, \u2026)` rule would carry\r\n the library default as its fallback and skip the [colors] tier. -->\r\n <text\r\n class=\"kerusi-seat__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"glyph.label.x\"\r\n [attr.y]=\"glyph.label.y\"\r\n [attr.font-size]=\"glyph.label.fontSize\"\r\n [style.fill]=\"glyph.label.fill\"\r\n [style.stroke]=\"glyph.label.halo\"\r\n [attr.stroke-width]=\"glyph.label.haloWidth\"\r\n [attr.transform]=\"glyph.label.transform\"\r\n >\r\n {{ glyph.label.text }}\r\n </text>\r\n @if (glyph.wheelchair; as wheelchair) {\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]=\"wheelchair.cx\"\r\n [attr.cy]=\"wheelchair.cy\"\r\n [attr.r]=\"wheelchair.r\"\r\n [style.fill]=\"wheelchair.fill\"\r\n />\r\n }\r\n </ng-template>\r\n\r\n @for (entry of seats(); track entry.placed.seat.id) {\r\n @let placed = entry.placed;\r\n @let glyph = entry.glyph;\r\n @let ctx =\r\n {\r\n placed: placed,\r\n seat: placed.seat,\r\n selected: entry.selected,\r\n glyph: glyph,\r\n defaultContent: stockSeat,\r\n };\r\n <g\r\n [class]=\"glyph.className\"\r\n role=\"button\"\r\n [attr.tabindex]=\"tabIndex(placed)\"\r\n [attr.aria-label]=\"ariaLabel(placed)\"\r\n [attr.aria-pressed]=\"entry.selected\"\r\n [attr.aria-disabled]=\"!placed.seat.selectable\"\r\n [attr.data-seat-id]=\"placed.seat.id\"\r\n [attr.data-seat-type]=\"glyph.data.type\"\r\n [attr.data-seat-status]=\"glyph.data.status\"\r\n [attr.data-section-id]=\"glyph.data.section\"\r\n [attr.data-seat-row]=\"glyph.data.row\"\r\n [attr.data-seat-attributes]=\"glyph.data.attributes\"\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 @if (seatTemplate(); as custom) {\r\n <ng-container *ngTemplateOutlet=\"custom; context: { $implicit: ctx }\" />\r\n <!-- Drawn over the custom content, because that content may have no\r\n .kerusi-seat__box for the stock focus ring to stroke. Only visible\r\n on :focus-visible. -->\r\n <path class=\"kerusi-seat__focus\" [attr.d]=\"glyph.body.d\" />\r\n } @else {\r\n <ng-container *ngTemplateOutlet=\"stockSeat; context: { $implicit: ctx }\" />\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-seat__focus{fill:none;stroke:none;pointer-events:none}.kerusi-seat:focus-visible .kerusi-seat__focus{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
148
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiSectionComponent, isStandalone: true, selector: "g[kerusi-section]", inputs: { layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, showLabel: { classPropertyName: "showLabel", publicName: "showLabel", isSignal: true, isRequired: false, 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 }, seatTypeStyles: { classPropertyName: "seatTypeStyles", publicName: "seatTypeStyles", isSignal: true, isRequired: false, transformFunction: null }, seatStyle: { classPropertyName: "seatStyle", publicName: "seatStyle", isSignal: true, isRequired: false, transformFunction: null }, seatTemplate: { classPropertyName: "seatTemplate", publicName: "seatTemplate", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { seatActivate: "seatActivate", seatFocused: "seatFocused", seatKeydown: "seatKeydown" }, host: { attributes: { "role": "group" }, properties: { "attr.aria-label": "label() ?? layout().section.id", "attr.data-section-id": "layout().section.id" }, classAttribute: "kerusi-section" }, ngImport: i0, template: "<!-- Hidden from assistive technology: the group already carries the same text\n as its accessible name. -->\n@if (drawsLabel()) {\n <svg:text\n class=\"kerusi-section-label\"\n aria-hidden=\"true\"\n text-anchor=\"start\"\n dominant-baseline=\"hanging\"\n [attr.x]=\"layout().labelAnchor.x\"\n [attr.y]=\"layout().labelAnchor.y\"\n [attr.font-size]=\"layout().labelAnchor.fontSize\"\n [style.fill]=\"labelFill()\"\n >\n {{ label() }}\n </svg:text>\n}\n\n<!-- Elements first, so a screen or stage sits behind the seats it faces. -->\n@for (placed of layout().elements; track placed.element.id) {\n @if (drawsElement(placed)) {\n <svg:g\n class=\"kerusi-element kerusi-element--{{ placed.element.kind }}\"\n [attr.transform]=\"transform(placed)\"\n >\n @switch (style(placed).shape) {\n @case ('screen') {\n <svg:path [attr.d]=\"screenPathOf(placed)\" [style.fill]=\"elementFillOf(placed)\" />\n }\n @case ('text') {}\n @case ('void') {\n <svg:rect\n class=\"kerusi-element__void\"\n [attr.x]=\"placed.x\"\n [attr.y]=\"placed.y\"\n [attr.width]=\"placed.width\"\n [attr.height]=\"placed.height\"\n [style.stroke]=\"elementTextFillOf(placed)\"\n fill=\"none\"\n />\n }\n @default {\n <svg:rect\n [attr.x]=\"placed.x\"\n [attr.y]=\"placed.y\"\n [attr.width]=\"placed.width\"\n [attr.height]=\"placed.height\"\n [attr.rx]=\"radiusOf(placed)\"\n [style.fill]=\"elementFillOf(placed)\"\n />\n }\n }\n @if (placed.element.label && style(placed).showLabel) {\n <svg:text\n class=\"kerusi-element__label\"\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n [attr.x]=\"placed.centerX\"\n [attr.y]=\"placed.centerY\"\n [attr.font-size]=\"placed.fontSize\"\n [style.fill]=\"elementTextFillOf(placed)\"\n [attr.transform]=\"labelTransform(placed)\"\n >\n {{ placed.element.label }}\n </svg:text>\n }\n </svg:g>\n }\n}\n\n<!-- The stock seat drawing. A template rather than inline markup so that a\n custom seat template can render it too, as `defaultContent`. Its `svg:`\n prefixes create its elements in the SVG namespace wherever it is stamped. -->\n<ng-template #stockSeat let-seat>\n @let glyph = seat.glyph;\n <!-- Tapered toward the back, square at the front, so which way the seat\n faces is legible from the outline alone. -->\n <svg:path class=\"kerusi-seat__box\" [attr.d]=\"glyph.body.d\" [style.fill]=\"glyph.body.fill\" />\n @if (glyph.wash; as wash) {\n <!-- Colour stays the seat type's; a taken seat recedes under a wash\n instead, so the type colour still reads on a busy map. -->\n <svg:path class=\"kerusi-seat__wash\" [attr.d]=\"wash.d\" [style.fill]=\"wash.fill\" />\n }\n @if (glyph.core; as core) {\n <!-- Selection is the one state drawn from two tones: the body above is\n the frame, this is the bright core inside it. Whichever way the\n host page is themed, one of the two separates from it \u2014 which is\n why nothing here has to know about the page. -->\n <svg:path class=\"kerusi-seat__core\" [attr.d]=\"core.d\" [style.fill]=\"core.fill\" />\n }\n @if (glyph.occupant; as occupant) {\n <!-- Deliberately not counter-rotated: the occupant leans with the\n seat. Solid = settled (yours, or sold); hollow = a hold still in\n progress. -->\n <svg:path\n class=\"kerusi-seat__occupant kerusi-seat__occupant--{{ occupant.variant }}\"\n [attr.d]=\"occupant.d\"\n [attr.stroke-width]=\"occupant.strokeWidth\"\n [style.fill]=\"occupant.fill\"\n [style.stroke]=\"occupant.stroke\"\n />\n }\n <!-- The halo colour and width are bound here rather than in the\n stylesheet: a CSS `var(--kerusi-selected-fg, \u2026)` rule would carry\n the library default as its fallback and skip the [colors] tier. -->\n <svg:text\n class=\"kerusi-seat__label\"\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n [attr.x]=\"glyph.label.x\"\n [attr.y]=\"glyph.label.y\"\n [attr.font-size]=\"glyph.label.fontSize\"\n [style.fill]=\"glyph.label.fill\"\n [style.stroke]=\"glyph.label.halo\"\n [attr.stroke-width]=\"glyph.label.haloWidth\"\n [attr.transform]=\"glyph.label.transform\"\n >\n {{ glyph.label.text }}\n </svg:text>\n @if (glyph.wheelchair; as wheelchair) {\n <!-- A corner marker, not a different fill: availability owns the fill. -->\n <svg:circle\n class=\"kerusi-seat__wheelchair\"\n [attr.cx]=\"wheelchair.cx\"\n [attr.cy]=\"wheelchair.cy\"\n [attr.r]=\"wheelchair.r\"\n [style.fill]=\"wheelchair.fill\"\n />\n }\n</ng-template>\n\n@for (entry of seats(); track entry.placed.seat.id) {\n @let placed = entry.placed;\n @let glyph = entry.glyph;\n @let ctx =\n {\n placed: placed,\n seat: placed.seat,\n selected: entry.selected,\n glyph: glyph,\n defaultContent: stockSeat,\n };\n <svg:g\n [class]=\"glyph.className\"\n role=\"button\"\n [attr.tabindex]=\"tabIndex(placed)\"\n [attr.aria-label]=\"ariaLabel(placed)\"\n [attr.aria-pressed]=\"entry.selected\"\n [attr.aria-disabled]=\"!placed.seat.selectable\"\n [attr.data-seat-id]=\"placed.seat.id\"\n [attr.data-seat-type]=\"glyph.data.type\"\n [attr.data-seat-status]=\"glyph.data.status\"\n [attr.data-section-id]=\"glyph.data.section\"\n [attr.data-seat-row]=\"glyph.data.row\"\n [attr.data-seat-attributes]=\"glyph.data.attributes\"\n [attr.transform]=\"transform(placed)\"\n (click)=\"onSeatClick(placed)\"\n (focus)=\"onSeatFocus(placed)\"\n (keydown)=\"onKeydown($event)\"\n >\n @if (seatTemplate(); as custom) {\n <ng-container *ngTemplateOutlet=\"custom; context: { $implicit: ctx }\" />\n <!-- Drawn over the custom content, because that content may have no\n .kerusi-seat__box for the stock focus ring to stroke. Only visible\n on :focus-visible. -->\n <svg:path class=\"kerusi-seat__focus\" [attr.d]=\"glyph.body.d\" />\n } @else {\n <ng-container *ngTemplateOutlet=\"stockSeat; context: { $implicit: ctx }\" />\n }\n </svg:g>\n}\n", styles: [".kerusi-section-label{font-family:inherit;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.75;pointer-events:none;-webkit-user-select:none;user-select:none}.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-seat__focus{fill:none;stroke:none;pointer-events:none}.kerusi-seat:focus-visible .kerusi-seat__focus{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
153
149
  }
154
150
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSectionComponent, decorators: [{
155
151
  type: Component,
156
- args: [{ selector: 'kerusi-section', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], 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 <!-- The stock seat drawing. A template rather than inline markup so that a\r\n custom seat template can render it too, as `defaultContent`. Declared\r\n inside the <svg>, so its elements are created in the SVG namespace. -->\r\n <ng-template #stockSeat let-seat>\r\n @let glyph = seat.glyph;\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]=\"glyph.body.d\" [style.fill]=\"glyph.body.fill\" />\r\n @if (glyph.wash; as wash) {\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 class=\"kerusi-seat__wash\" [attr.d]=\"wash.d\" [style.fill]=\"wash.fill\" />\r\n }\r\n @if (glyph.core; as core) {\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]=\"core.d\" [style.fill]=\"core.fill\" />\r\n }\r\n @if (glyph.occupant; as occupant) {\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--{{ occupant.variant }}\"\r\n [attr.d]=\"occupant.d\"\r\n [attr.stroke-width]=\"occupant.strokeWidth\"\r\n [style.fill]=\"occupant.fill\"\r\n [style.stroke]=\"occupant.stroke\"\r\n />\r\n }\r\n <!-- The halo colour and width are bound here rather than in the\r\n stylesheet: a CSS `var(--kerusi-selected-fg, \u2026)` rule would carry\r\n the library default as its fallback and skip the [colors] tier. -->\r\n <text\r\n class=\"kerusi-seat__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"glyph.label.x\"\r\n [attr.y]=\"glyph.label.y\"\r\n [attr.font-size]=\"glyph.label.fontSize\"\r\n [style.fill]=\"glyph.label.fill\"\r\n [style.stroke]=\"glyph.label.halo\"\r\n [attr.stroke-width]=\"glyph.label.haloWidth\"\r\n [attr.transform]=\"glyph.label.transform\"\r\n >\r\n {{ glyph.label.text }}\r\n </text>\r\n @if (glyph.wheelchair; as wheelchair) {\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]=\"wheelchair.cx\"\r\n [attr.cy]=\"wheelchair.cy\"\r\n [attr.r]=\"wheelchair.r\"\r\n [style.fill]=\"wheelchair.fill\"\r\n />\r\n }\r\n </ng-template>\r\n\r\n @for (entry of seats(); track entry.placed.seat.id) {\r\n @let placed = entry.placed;\r\n @let glyph = entry.glyph;\r\n @let ctx =\r\n {\r\n placed: placed,\r\n seat: placed.seat,\r\n selected: entry.selected,\r\n glyph: glyph,\r\n defaultContent: stockSeat,\r\n };\r\n <g\r\n [class]=\"glyph.className\"\r\n role=\"button\"\r\n [attr.tabindex]=\"tabIndex(placed)\"\r\n [attr.aria-label]=\"ariaLabel(placed)\"\r\n [attr.aria-pressed]=\"entry.selected\"\r\n [attr.aria-disabled]=\"!placed.seat.selectable\"\r\n [attr.data-seat-id]=\"placed.seat.id\"\r\n [attr.data-seat-type]=\"glyph.data.type\"\r\n [attr.data-seat-status]=\"glyph.data.status\"\r\n [attr.data-section-id]=\"glyph.data.section\"\r\n [attr.data-seat-row]=\"glyph.data.row\"\r\n [attr.data-seat-attributes]=\"glyph.data.attributes\"\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 @if (seatTemplate(); as custom) {\r\n <ng-container *ngTemplateOutlet=\"custom; context: { $implicit: ctx }\" />\r\n <!-- Drawn over the custom content, because that content may have no\r\n .kerusi-seat__box for the stock focus ring to stroke. Only visible\r\n on :focus-visible. -->\r\n <path class=\"kerusi-seat__focus\" [attr.d]=\"glyph.body.d\" />\r\n } @else {\r\n <ng-container *ngTemplateOutlet=\"stockSeat; context: { $implicit: ctx }\" />\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-seat__focus{fill:none;stroke:none;pointer-events:none}.kerusi-seat:focus-visible .kerusi-seat__focus{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"] }]
157
- }], 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 }] }], seatTypeStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTypeStyles", required: false }] }], seatStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatStyle", required: false }] }], seatTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTemplate", 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"] }] } });
152
+ args: [{ selector: 'g[kerusi-section]', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], host: {
153
+ class: 'kerusi-section',
154
+ role: 'group',
155
+ '[attr.aria-label]': 'label() ?? layout().section.id',
156
+ '[attr.data-section-id]': 'layout().section.id',
157
+ }, template: "<!-- Hidden from assistive technology: the group already carries the same text\n as its accessible name. -->\n@if (drawsLabel()) {\n <svg:text\n class=\"kerusi-section-label\"\n aria-hidden=\"true\"\n text-anchor=\"start\"\n dominant-baseline=\"hanging\"\n [attr.x]=\"layout().labelAnchor.x\"\n [attr.y]=\"layout().labelAnchor.y\"\n [attr.font-size]=\"layout().labelAnchor.fontSize\"\n [style.fill]=\"labelFill()\"\n >\n {{ label() }}\n </svg:text>\n}\n\n<!-- Elements first, so a screen or stage sits behind the seats it faces. -->\n@for (placed of layout().elements; track placed.element.id) {\n @if (drawsElement(placed)) {\n <svg:g\n class=\"kerusi-element kerusi-element--{{ placed.element.kind }}\"\n [attr.transform]=\"transform(placed)\"\n >\n @switch (style(placed).shape) {\n @case ('screen') {\n <svg:path [attr.d]=\"screenPathOf(placed)\" [style.fill]=\"elementFillOf(placed)\" />\n }\n @case ('text') {}\n @case ('void') {\n <svg:rect\n class=\"kerusi-element__void\"\n [attr.x]=\"placed.x\"\n [attr.y]=\"placed.y\"\n [attr.width]=\"placed.width\"\n [attr.height]=\"placed.height\"\n [style.stroke]=\"elementTextFillOf(placed)\"\n fill=\"none\"\n />\n }\n @default {\n <svg:rect\n [attr.x]=\"placed.x\"\n [attr.y]=\"placed.y\"\n [attr.width]=\"placed.width\"\n [attr.height]=\"placed.height\"\n [attr.rx]=\"radiusOf(placed)\"\n [style.fill]=\"elementFillOf(placed)\"\n />\n }\n }\n @if (placed.element.label && style(placed).showLabel) {\n <svg:text\n class=\"kerusi-element__label\"\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n [attr.x]=\"placed.centerX\"\n [attr.y]=\"placed.centerY\"\n [attr.font-size]=\"placed.fontSize\"\n [style.fill]=\"elementTextFillOf(placed)\"\n [attr.transform]=\"labelTransform(placed)\"\n >\n {{ placed.element.label }}\n </svg:text>\n }\n </svg:g>\n }\n}\n\n<!-- The stock seat drawing. A template rather than inline markup so that a\n custom seat template can render it too, as `defaultContent`. Its `svg:`\n prefixes create its elements in the SVG namespace wherever it is stamped. -->\n<ng-template #stockSeat let-seat>\n @let glyph = seat.glyph;\n <!-- Tapered toward the back, square at the front, so which way the seat\n faces is legible from the outline alone. -->\n <svg:path class=\"kerusi-seat__box\" [attr.d]=\"glyph.body.d\" [style.fill]=\"glyph.body.fill\" />\n @if (glyph.wash; as wash) {\n <!-- Colour stays the seat type's; a taken seat recedes under a wash\n instead, so the type colour still reads on a busy map. -->\n <svg:path class=\"kerusi-seat__wash\" [attr.d]=\"wash.d\" [style.fill]=\"wash.fill\" />\n }\n @if (glyph.core; as core) {\n <!-- Selection is the one state drawn from two tones: the body above is\n the frame, this is the bright core inside it. Whichever way the\n host page is themed, one of the two separates from it \u2014 which is\n why nothing here has to know about the page. -->\n <svg:path class=\"kerusi-seat__core\" [attr.d]=\"core.d\" [style.fill]=\"core.fill\" />\n }\n @if (glyph.occupant; as occupant) {\n <!-- Deliberately not counter-rotated: the occupant leans with the\n seat. Solid = settled (yours, or sold); hollow = a hold still in\n progress. -->\n <svg:path\n class=\"kerusi-seat__occupant kerusi-seat__occupant--{{ occupant.variant }}\"\n [attr.d]=\"occupant.d\"\n [attr.stroke-width]=\"occupant.strokeWidth\"\n [style.fill]=\"occupant.fill\"\n [style.stroke]=\"occupant.stroke\"\n />\n }\n <!-- The halo colour and width are bound here rather than in the\n stylesheet: a CSS `var(--kerusi-selected-fg, \u2026)` rule would carry\n the library default as its fallback and skip the [colors] tier. -->\n <svg:text\n class=\"kerusi-seat__label\"\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n [attr.x]=\"glyph.label.x\"\n [attr.y]=\"glyph.label.y\"\n [attr.font-size]=\"glyph.label.fontSize\"\n [style.fill]=\"glyph.label.fill\"\n [style.stroke]=\"glyph.label.halo\"\n [attr.stroke-width]=\"glyph.label.haloWidth\"\n [attr.transform]=\"glyph.label.transform\"\n >\n {{ glyph.label.text }}\n </svg:text>\n @if (glyph.wheelchair; as wheelchair) {\n <!-- A corner marker, not a different fill: availability owns the fill. -->\n <svg:circle\n class=\"kerusi-seat__wheelchair\"\n [attr.cx]=\"wheelchair.cx\"\n [attr.cy]=\"wheelchair.cy\"\n [attr.r]=\"wheelchair.r\"\n [style.fill]=\"wheelchair.fill\"\n />\n }\n</ng-template>\n\n@for (entry of seats(); track entry.placed.seat.id) {\n @let placed = entry.placed;\n @let glyph = entry.glyph;\n @let ctx =\n {\n placed: placed,\n seat: placed.seat,\n selected: entry.selected,\n glyph: glyph,\n defaultContent: stockSeat,\n };\n <svg:g\n [class]=\"glyph.className\"\n role=\"button\"\n [attr.tabindex]=\"tabIndex(placed)\"\n [attr.aria-label]=\"ariaLabel(placed)\"\n [attr.aria-pressed]=\"entry.selected\"\n [attr.aria-disabled]=\"!placed.seat.selectable\"\n [attr.data-seat-id]=\"placed.seat.id\"\n [attr.data-seat-type]=\"glyph.data.type\"\n [attr.data-seat-status]=\"glyph.data.status\"\n [attr.data-section-id]=\"glyph.data.section\"\n [attr.data-seat-row]=\"glyph.data.row\"\n [attr.data-seat-attributes]=\"glyph.data.attributes\"\n [attr.transform]=\"transform(placed)\"\n (click)=\"onSeatClick(placed)\"\n (focus)=\"onSeatFocus(placed)\"\n (keydown)=\"onKeydown($event)\"\n >\n @if (seatTemplate(); as custom) {\n <ng-container *ngTemplateOutlet=\"custom; context: { $implicit: ctx }\" />\n <!-- Drawn over the custom content, because that content may have no\n .kerusi-seat__box for the stock focus ring to stroke. Only visible\n on :focus-visible. -->\n <svg:path class=\"kerusi-seat__focus\" [attr.d]=\"glyph.body.d\" />\n } @else {\n <ng-container *ngTemplateOutlet=\"stockSeat; context: { $implicit: ctx }\" />\n }\n </svg:g>\n}\n", styles: [".kerusi-section-label{font-family:inherit;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.75;pointer-events:none;-webkit-user-select:none;user-select:none}.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-seat__focus{fill:none;stroke:none;pointer-events:none}.kerusi-seat:focus-visible .kerusi-seat__focus{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"] }]
158
+ }], propDecorators: { layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], showLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLabel", required: false }] }], 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 }] }], seatTypeStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTypeStyles", required: false }] }], seatStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatStyle", required: false }] }], seatTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTemplate", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], seatActivate: [{ type: i0.Output, args: ["seatActivate"] }], seatFocused: [{ type: i0.Output, args: ["seatFocused"] }], seatKeydown: [{ type: i0.Output, args: ["seatKeydown"] }] } });
158
159
 
159
160
  /**
160
161
  * A key for a seat map: its seat types on one side, its availability states on
@@ -274,11 +275,11 @@ class KerusiLegendComponent {
274
275
  return entry.price ? formatMoney(entry.price, this.locale()) : '';
275
276
  }
276
277
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiLegendComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
277
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiLegendComponent, isStandalone: true, selector: "kerusi-legend", inputs: { legend: { classPropertyName: "legend", publicName: "legend", isSignal: true, isRequired: true, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", 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 }, seatTypeStyles: { classPropertyName: "seatTypeStyles", publicName: "seatTypeStyles", isSignal: true, isRequired: false, transformFunction: null }, showPrices: { classPropertyName: "showPrices", publicName: "showPrices", isSignal: true, isRequired: false, transformFunction: null }, hideUnusedTypes: { classPropertyName: "hideUnusedTypes", publicName: "hideUnusedTypes", isSignal: true, isRequired: false, transformFunction: null }, headingSeatTypes: { classPropertyName: "headingSeatTypes", publicName: "headingSeatTypes", isSignal: true, isRequired: false, transformFunction: null }, headingAvailability: { classPropertyName: "headingAvailability", publicName: "headingAvailability", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"kerusi-legend\">\r\n @if (types().length) {\r\n <div class=\"kerusi-legend__block\">\r\n <h4 class=\"kerusi-legend__heading\">{{ headingSeatTypes() }}</h4>\r\n <ul class=\"kerusi-legend__list\">\r\n @for (entry of types(); track entry.id) {\r\n <li class=\"kerusi-legend__item\" [attr.data-seat-type]=\"entry.id\">\r\n @if (typeShape(entry); as d) {\r\n <!-- A type styled with its own shape keys that shape, not a square. -->\r\n <svg\r\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\r\n aria-hidden=\"true\"\r\n focusable=\"false\"\r\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\r\n >\r\n <path [attr.d]=\"d\" [style.fill]=\"swatch(entry)\" />\r\n </svg>\r\n } @else {\r\n <span\r\n class=\"kerusi-legend__swatch\"\r\n aria-hidden=\"true\"\r\n [style.background]=\"swatch(entry)\"\r\n [style.color]=\"swatchText(entry)\"\r\n ></span>\r\n }\r\n <span class=\"kerusi-legend__label\">{{ entry.label }}</span>\r\n @if (showPrices() && price(entry)) {\r\n <span class=\"kerusi-legend__price\">{{ price(entry) }}</span>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n </div>\r\n }\r\n\r\n <div class=\"kerusi-legend__block\">\r\n <h4 class=\"kerusi-legend__heading\">{{ headingAvailability() }}</h4>\r\n <ul class=\"kerusi-legend__list\">\r\n @for (status of statuses(); track status.key) {\r\n <li class=\"kerusi-legend__item\">\r\n <!-- Shape carries status now, so every swatch draws the seat glyph\r\n rather than a flat colour \u2014 a flat swatch would teach a cue the\r\n map itself no longer uses. -->\r\n <svg\r\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\r\n aria-hidden=\"true\"\r\n focusable=\"false\"\r\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\r\n >\r\n <path [attr.d]=\"glyphBody\" [style.fill]=\"status.fill\" />\r\n @if (hasWash(status.kind)) {\r\n <path [attr.d]=\"glyphBody\" [style.fill]=\"washFillOf(status.kind)\" opacity=\"0.5\" />\r\n }\r\n @if (status.kind === 'selected') {\r\n <!-- Two tones, same as the map: the body above is the frame, this\r\n is the core inside it. -->\r\n <path [attr.d]=\"glyphCore\" [style.fill]=\"coreFill()\" />\r\n }\r\n @if (hasOccupant(status.kind)) {\r\n <path\r\n [attr.d]=\"occupantPathOf(status.kind)\"\r\n class=\"kerusi-legend__occupant kerusi-legend__occupant--{{ status.kind }}\"\r\n [attr.stroke-width]=\"occupantStrokeWidthOf(status.kind)\"\r\n [style.fill]=\"occupantFillOf(status.kind)\"\r\n [style.stroke]=\"occupantStrokeOf(status.kind)\"\r\n />\r\n }\r\n </svg>\r\n <span class=\"kerusi-legend__label\">{{ status.label }}</span>\r\n </li>\r\n }\r\n </ul>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block}.kerusi-legend{display:flex;flex-wrap:wrap;gap:1.5rem;font-size:.85rem}.kerusi-legend__block{display:flex;flex-direction:column;gap:.4rem;min-width:0}.kerusi-legend__heading{margin:0;font-size:.72rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;opacity:.65}.kerusi-legend__list{display:flex;flex-wrap:wrap;gap:.35rem .9rem;margin:0;padding:0;list-style:none}.kerusi-legend__item{display:inline-flex;align-items:center;gap:.4rem}.kerusi-legend__swatch{display:inline-block;flex:none;width:.85rem;height:.85rem;border-radius:3px}.kerusi-legend__swatch--glyph{border-radius:0;overflow:visible}.kerusi-legend__occupant--selected{opacity:.2}.kerusi-legend__occupant--booked{opacity:.55}.kerusi-legend__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-legend__price{opacity:.7;font-variant-numeric:tabular-nums}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
278
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiLegendComponent, isStandalone: true, selector: "kerusi-legend", inputs: { legend: { classPropertyName: "legend", publicName: "legend", isSignal: true, isRequired: true, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", 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 }, seatTypeStyles: { classPropertyName: "seatTypeStyles", publicName: "seatTypeStyles", isSignal: true, isRequired: false, transformFunction: null }, showPrices: { classPropertyName: "showPrices", publicName: "showPrices", isSignal: true, isRequired: false, transformFunction: null }, hideUnusedTypes: { classPropertyName: "hideUnusedTypes", publicName: "hideUnusedTypes", isSignal: true, isRequired: false, transformFunction: null }, headingSeatTypes: { classPropertyName: "headingSeatTypes", publicName: "headingSeatTypes", isSignal: true, isRequired: false, transformFunction: null }, headingAvailability: { classPropertyName: "headingAvailability", publicName: "headingAvailability", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"kerusi-legend\">\n @if (types().length) {\n <div class=\"kerusi-legend__block\">\n <h4 class=\"kerusi-legend__heading\">{{ headingSeatTypes() }}</h4>\n <ul class=\"kerusi-legend__list\">\n @for (entry of types(); track entry.id) {\n <li class=\"kerusi-legend__item\" [attr.data-seat-type]=\"entry.id\">\n @if (typeShape(entry); as d) {\n <!-- A type styled with its own shape keys that shape, not a square. -->\n <svg\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\n aria-hidden=\"true\"\n focusable=\"false\"\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\n >\n <path [attr.d]=\"d\" [style.fill]=\"swatch(entry)\" />\n </svg>\n } @else {\n <span\n class=\"kerusi-legend__swatch\"\n aria-hidden=\"true\"\n [style.background]=\"swatch(entry)\"\n [style.color]=\"swatchText(entry)\"\n ></span>\n }\n <span class=\"kerusi-legend__label\">{{ entry.label }}</span>\n @if (showPrices() && price(entry)) {\n <span class=\"kerusi-legend__price\">{{ price(entry) }}</span>\n }\n </li>\n }\n </ul>\n </div>\n }\n\n <div class=\"kerusi-legend__block\">\n <h4 class=\"kerusi-legend__heading\">{{ headingAvailability() }}</h4>\n <ul class=\"kerusi-legend__list\">\n @for (status of statuses(); track status.key) {\n <li class=\"kerusi-legend__item\">\n <!-- Shape carries status now, so every swatch draws the seat glyph\n rather than a flat colour \u2014 a flat swatch would teach a cue the\n map itself no longer uses. -->\n <svg\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\n aria-hidden=\"true\"\n focusable=\"false\"\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\n >\n <path [attr.d]=\"glyphBody\" [style.fill]=\"status.fill\" />\n @if (hasWash(status.kind)) {\n <path [attr.d]=\"glyphBody\" [style.fill]=\"washFillOf(status.kind)\" opacity=\"0.5\" />\n }\n @if (status.kind === 'selected') {\n <!-- Two tones, same as the map: the body above is the frame, this\n is the core inside it. -->\n <path [attr.d]=\"glyphCore\" [style.fill]=\"coreFill()\" />\n }\n @if (hasOccupant(status.kind)) {\n <path\n [attr.d]=\"occupantPathOf(status.kind)\"\n class=\"kerusi-legend__occupant kerusi-legend__occupant--{{ status.kind }}\"\n [attr.stroke-width]=\"occupantStrokeWidthOf(status.kind)\"\n [style.fill]=\"occupantFillOf(status.kind)\"\n [style.stroke]=\"occupantStrokeOf(status.kind)\"\n />\n }\n </svg>\n <span class=\"kerusi-legend__label\">{{ status.label }}</span>\n </li>\n }\n </ul>\n </div>\n</div>\n", styles: [":host{display:block}.kerusi-legend{display:flex;flex-wrap:wrap;gap:1.5rem;font-size:.85rem}.kerusi-legend__block{display:flex;flex-direction:column;gap:.4rem;min-width:0}.kerusi-legend__heading{margin:0;font-size:.72rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;opacity:.65}.kerusi-legend__list{display:flex;flex-wrap:wrap;gap:.35rem .9rem;margin:0;padding:0;list-style:none}.kerusi-legend__item{display:inline-flex;align-items:center;gap:.4rem}.kerusi-legend__swatch{display:inline-block;flex:none;width:.85rem;height:.85rem;border-radius:3px}.kerusi-legend__swatch--glyph{border-radius:0;overflow:visible}.kerusi-legend__occupant--selected{opacity:.2}.kerusi-legend__occupant--booked{opacity:.55}.kerusi-legend__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-legend__price{opacity:.7;font-variant-numeric:tabular-nums}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
278
279
  }
279
280
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiLegendComponent, decorators: [{
280
281
  type: Component,
281
- args: [{ selector: 'kerusi-legend', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"kerusi-legend\">\r\n @if (types().length) {\r\n <div class=\"kerusi-legend__block\">\r\n <h4 class=\"kerusi-legend__heading\">{{ headingSeatTypes() }}</h4>\r\n <ul class=\"kerusi-legend__list\">\r\n @for (entry of types(); track entry.id) {\r\n <li class=\"kerusi-legend__item\" [attr.data-seat-type]=\"entry.id\">\r\n @if (typeShape(entry); as d) {\r\n <!-- A type styled with its own shape keys that shape, not a square. -->\r\n <svg\r\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\r\n aria-hidden=\"true\"\r\n focusable=\"false\"\r\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\r\n >\r\n <path [attr.d]=\"d\" [style.fill]=\"swatch(entry)\" />\r\n </svg>\r\n } @else {\r\n <span\r\n class=\"kerusi-legend__swatch\"\r\n aria-hidden=\"true\"\r\n [style.background]=\"swatch(entry)\"\r\n [style.color]=\"swatchText(entry)\"\r\n ></span>\r\n }\r\n <span class=\"kerusi-legend__label\">{{ entry.label }}</span>\r\n @if (showPrices() && price(entry)) {\r\n <span class=\"kerusi-legend__price\">{{ price(entry) }}</span>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n </div>\r\n }\r\n\r\n <div class=\"kerusi-legend__block\">\r\n <h4 class=\"kerusi-legend__heading\">{{ headingAvailability() }}</h4>\r\n <ul class=\"kerusi-legend__list\">\r\n @for (status of statuses(); track status.key) {\r\n <li class=\"kerusi-legend__item\">\r\n <!-- Shape carries status now, so every swatch draws the seat glyph\r\n rather than a flat colour \u2014 a flat swatch would teach a cue the\r\n map itself no longer uses. -->\r\n <svg\r\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\r\n aria-hidden=\"true\"\r\n focusable=\"false\"\r\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\r\n >\r\n <path [attr.d]=\"glyphBody\" [style.fill]=\"status.fill\" />\r\n @if (hasWash(status.kind)) {\r\n <path [attr.d]=\"glyphBody\" [style.fill]=\"washFillOf(status.kind)\" opacity=\"0.5\" />\r\n }\r\n @if (status.kind === 'selected') {\r\n <!-- Two tones, same as the map: the body above is the frame, this\r\n is the core inside it. -->\r\n <path [attr.d]=\"glyphCore\" [style.fill]=\"coreFill()\" />\r\n }\r\n @if (hasOccupant(status.kind)) {\r\n <path\r\n [attr.d]=\"occupantPathOf(status.kind)\"\r\n class=\"kerusi-legend__occupant kerusi-legend__occupant--{{ status.kind }}\"\r\n [attr.stroke-width]=\"occupantStrokeWidthOf(status.kind)\"\r\n [style.fill]=\"occupantFillOf(status.kind)\"\r\n [style.stroke]=\"occupantStrokeOf(status.kind)\"\r\n />\r\n }\r\n </svg>\r\n <span class=\"kerusi-legend__label\">{{ status.label }}</span>\r\n </li>\r\n }\r\n </ul>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block}.kerusi-legend{display:flex;flex-wrap:wrap;gap:1.5rem;font-size:.85rem}.kerusi-legend__block{display:flex;flex-direction:column;gap:.4rem;min-width:0}.kerusi-legend__heading{margin:0;font-size:.72rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;opacity:.65}.kerusi-legend__list{display:flex;flex-wrap:wrap;gap:.35rem .9rem;margin:0;padding:0;list-style:none}.kerusi-legend__item{display:inline-flex;align-items:center;gap:.4rem}.kerusi-legend__swatch{display:inline-block;flex:none;width:.85rem;height:.85rem;border-radius:3px}.kerusi-legend__swatch--glyph{border-radius:0;overflow:visible}.kerusi-legend__occupant--selected{opacity:.2}.kerusi-legend__occupant--booked{opacity:.55}.kerusi-legend__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-legend__price{opacity:.7;font-variant-numeric:tabular-nums}\n"] }]
282
+ args: [{ selector: 'kerusi-legend', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"kerusi-legend\">\n @if (types().length) {\n <div class=\"kerusi-legend__block\">\n <h4 class=\"kerusi-legend__heading\">{{ headingSeatTypes() }}</h4>\n <ul class=\"kerusi-legend__list\">\n @for (entry of types(); track entry.id) {\n <li class=\"kerusi-legend__item\" [attr.data-seat-type]=\"entry.id\">\n @if (typeShape(entry); as d) {\n <!-- A type styled with its own shape keys that shape, not a square. -->\n <svg\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\n aria-hidden=\"true\"\n focusable=\"false\"\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\n >\n <path [attr.d]=\"d\" [style.fill]=\"swatch(entry)\" />\n </svg>\n } @else {\n <span\n class=\"kerusi-legend__swatch\"\n aria-hidden=\"true\"\n [style.background]=\"swatch(entry)\"\n [style.color]=\"swatchText(entry)\"\n ></span>\n }\n <span class=\"kerusi-legend__label\">{{ entry.label }}</span>\n @if (showPrices() && price(entry)) {\n <span class=\"kerusi-legend__price\">{{ price(entry) }}</span>\n }\n </li>\n }\n </ul>\n </div>\n }\n\n <div class=\"kerusi-legend__block\">\n <h4 class=\"kerusi-legend__heading\">{{ headingAvailability() }}</h4>\n <ul class=\"kerusi-legend__list\">\n @for (status of statuses(); track status.key) {\n <li class=\"kerusi-legend__item\">\n <!-- Shape carries status now, so every swatch draws the seat glyph\n rather than a flat colour \u2014 a flat swatch would teach a cue the\n map itself no longer uses. -->\n <svg\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\n aria-hidden=\"true\"\n focusable=\"false\"\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\n >\n <path [attr.d]=\"glyphBody\" [style.fill]=\"status.fill\" />\n @if (hasWash(status.kind)) {\n <path [attr.d]=\"glyphBody\" [style.fill]=\"washFillOf(status.kind)\" opacity=\"0.5\" />\n }\n @if (status.kind === 'selected') {\n <!-- Two tones, same as the map: the body above is the frame, this\n is the core inside it. -->\n <path [attr.d]=\"glyphCore\" [style.fill]=\"coreFill()\" />\n }\n @if (hasOccupant(status.kind)) {\n <path\n [attr.d]=\"occupantPathOf(status.kind)\"\n class=\"kerusi-legend__occupant kerusi-legend__occupant--{{ status.kind }}\"\n [attr.stroke-width]=\"occupantStrokeWidthOf(status.kind)\"\n [style.fill]=\"occupantFillOf(status.kind)\"\n [style.stroke]=\"occupantStrokeOf(status.kind)\"\n />\n }\n </svg>\n <span class=\"kerusi-legend__label\">{{ status.label }}</span>\n </li>\n }\n </ul>\n </div>\n</div>\n", styles: [":host{display:block}.kerusi-legend{display:flex;flex-wrap:wrap;gap:1.5rem;font-size:.85rem}.kerusi-legend__block{display:flex;flex-direction:column;gap:.4rem;min-width:0}.kerusi-legend__heading{margin:0;font-size:.72rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;opacity:.65}.kerusi-legend__list{display:flex;flex-wrap:wrap;gap:.35rem .9rem;margin:0;padding:0;list-style:none}.kerusi-legend__item{display:inline-flex;align-items:center;gap:.4rem}.kerusi-legend__swatch{display:inline-block;flex:none;width:.85rem;height:.85rem;border-radius:3px}.kerusi-legend__swatch--glyph{border-radius:0;overflow:visible}.kerusi-legend__occupant--selected{opacity:.2}.kerusi-legend__occupant--booked{opacity:.55}.kerusi-legend__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-legend__price{opacity:.7;font-variant-numeric:tabular-nums}\n"] }]
282
283
  }], propDecorators: { legend: [{ type: i0.Input, args: [{ isSignal: true, alias: "legend", required: true }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], seatTypeStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTypeStyles", required: false }] }], showPrices: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPrices", required: false }] }], hideUnusedTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideUnusedTypes", required: false }] }], headingSeatTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingSeatTypes", required: false }] }], headingAvailability: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingAvailability", required: false }] }] } });
283
284
 
284
285
  /**
@@ -317,8 +318,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
317
318
  * Renders a Kerusi seat map.
318
319
  *
319
320
  * Takes a `KerusiMap` and its `KerusiState` directly — no adapter, no
320
- * intermediate row model. Each `Section` becomes its own `<svg>` with its own
321
- * layout mode and proportions, seats take their color from the map's legend,
321
+ * intermediate row model. The map is one `<svg>` whose `viewBox` is the
322
+ * document's canvas (§4.5), each `Section` is a group on it, every seat is
323
+ * drawn exactly where and as large as the document says, seats take their
324
+ * color from the map's legend,
322
325
  * every seat is keyboard-reachable and announced with its type, price, status
323
326
  * and §4.3.4 accessibility properties, and selection is an immutable set of
324
327
  * seat ids the caller can two-way bind.
@@ -381,21 +384,19 @@ class KerusiSeatmapComponent {
381
384
  */
382
385
  seatTemplate = contentChild(KerusiSeatTemplateDirective, /* @ts-ignore */
383
386
  ...(ngDevMode ? [{ debugName: "seatTemplate" }] : /* istanbul ignore next */ []));
384
- /** Grid cell edge, in viewBox units. */
385
- seatSize = input(28, /* @ts-ignore */
386
- ...(ngDevMode ? [{ debugName: "seatSize" }] : /* istanbul ignore next */ []));
387
- /** Gap between grid cells, in viewBox units. */
388
- seatGap = input(6, /* @ts-ignore */
389
- ...(ngDevMode ? [{ debugName: "seatGap" }] : /* istanbul ignore next */ []));
390
- /** Freeform viewBox width; height follows the section's aspect ratio. */
391
- freeformBasis = input(1000, /* @ts-ignore */
392
- ...(ngDevMode ? [{ debugName: "freeformBasis" }] : /* istanbul ignore next */ []));
393
387
  /**
394
- * CSS pixels per viewBox unit. Caps each section at its natural size so a
395
- * narrow section and a wide one in the same map draw seats the same size.
388
+ * CSS pixels per canvas unit, at most: the map never draws wider than
389
+ * `canvas.width × unitScale`, and scales down to a narrower container. The
390
+ * document fixes every seat's size and position (§4.5); this is the only
391
+ * sizing control. Default `1`.
396
392
  */
397
393
  unitScale = input(1, /* @ts-ignore */
398
394
  ...(ngDevMode ? [{ debugName: "unitScale" }] : /* istanbul ignore next */ []));
395
+ /**
396
+ * Draws each section's label inside the map, at the top-left of its `bounds`.
397
+ * Only sections that declare `bounds` get one; every section's label is its
398
+ * group's accessible name either way. Default `true`.
399
+ */
399
400
  showSectionLabels = input(true, /* @ts-ignore */
400
401
  ...(ngDevMode ? [{ debugName: "showSectionLabels" }] : /* istanbul ignore next */ []));
401
402
  showLegend = input(false, /* @ts-ignore */
@@ -677,24 +678,19 @@ class KerusiSeatmapComponent {
677
678
  ...(ngDevMode ? [{ debugName: "atMaxZoom" }] : /* istanbul ignore next */ []));
678
679
  atFit = computed(() => this.viewportState().zoom <= 1 + 1e-4, /* @ts-ignore */
679
680
  ...(ngDevMode ? [{ debugName: "atFit" }] : /* istanbul ignore next */ []));
680
- /** Placed geometry per section, keyed by section id. */
681
- layouts = computed(() => {
682
- const overrides = this.sectionOverrides();
683
- return new Map(this.renderMap().sections.map((section) => {
684
- const override = overrides[section.id] ?? {};
685
- return [
686
- section.id,
687
- computeSectionLayout(section, {
688
- seatSize: override.seatSize ?? this.seatSize(),
689
- seatGap: this.seatGap(),
690
- freeformBasis: this.freeformBasis(),
691
- aspectRatio: override.aspectRatio,
692
- rtl: this.isRtl(),
693
- }),
694
- ];
695
- }));
696
- }, /* @ts-ignore */
697
- ...(ngDevMode ? [{ debugName: "layouts" }] : /* istanbul ignore next */ []));
681
+ /** Every seat and element placed on the canvas, mirrored as a whole in RTL (D3). */
682
+ layout = computed(() => computeMapLayout(this.renderMap(), { rtl: this.isRtl() }), /* @ts-ignore */
683
+ ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
684
+ viewBox = computed(() => `0 0 ${this.layout().width} ${this.layout().height}`, /* @ts-ignore */
685
+ ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
686
+ /** `unitScale` CSS pixels per canvas unit, at most. */
687
+ naturalWidth = computed(() => this.layout().width * this.unitScale(), /* @ts-ignore */
688
+ ...(ngDevMode ? [{ debugName: "naturalWidth" }] : /* istanbul ignore next */ []));
689
+ /** The canvas's width over its height, so a maximized map can fit the whole canvas. */
690
+ canvasAspect = computed(() => String(this.layout().width / this.layout().height), /* @ts-ignore */
691
+ ...(ngDevMode ? [{ debugName: "canvasAspect" }] : /* istanbul ignore next */ []));
692
+ backdrop = computed(() => cssVar('backdrop', this.resolvedColors().backdrop), /* @ts-ignore */
693
+ ...(ngDevMode ? [{ debugName: "backdrop" }] : /* istanbul ignore next */ []));
698
694
  /** Navigation graphs per section — arrows move within, Tab moves between. */
699
695
  graphs = computed(() => new Map(this.renderMap().sections.map((section) => [
700
696
  section.id,
@@ -719,15 +715,9 @@ class KerusiSeatmapComponent {
719
715
  summary = computed(() => summarizeSelection(this.renderMap(), this.selection()), /* @ts-ignore */
720
716
  ...(ngDevMode ? [{ debugName: "summary" }] : /* istanbul ignore next */ []));
721
717
  // --- template helpers -----------------------------------------------------
722
- layoutFor(section) {
723
- return this.layouts().get(section.id);
724
- }
725
718
  labelFor(section) {
726
719
  return this.sectionOverrides()[section.id]?.label ?? section.label;
727
720
  }
728
- labelId(section) {
729
- return `kerusi-section-${this.renderMap().id}-${section.id}`;
730
- }
731
721
  /** The seat holding this section's tab stop. */
732
722
  focusedSeatIn(section) {
733
723
  return this.focusedSeatIds()[section.id] ?? null;
@@ -770,9 +760,9 @@ class KerusiSeatmapComponent {
770
760
  }
771
761
  }
772
762
  /**
773
- * Arrow keys walk the navigation graph, which orders seats by `col` in grid
774
- * and mixed sections — §4.3.1's logical adjacency — so the aisle that is a
775
- * skipped column is stepped across rather than into.
763
+ * Arrow keys walk the navigation graph, which follows §4.3.5 adjacency:
764
+ * along a row by `col`, across an aisle (a skipped `col`) rather than into
765
+ * it, and between rows to the seat nearest on the canvas (D6).
776
766
  */
777
767
  onKeydown(section, event) {
778
768
  const seatId = event.target?.dataset?.['seatId'];
@@ -875,13 +865,40 @@ class KerusiSeatmapComponent {
875
865
  zoomOut() {
876
866
  this.stepZoom(-1);
877
867
  }
878
- /** Back to fit-to-container: the map's original view. No-op unless `zoomable`. */
868
+ /** Back to fit-to-container: the whole canvas. No-op unless `zoomable`. */
879
869
  resetZoom() {
880
870
  const measured = this.measure();
881
871
  if (measured && this.zoomable()) {
882
872
  this.commit(fitViewport(measured.content, measured.container, this.limits()), true);
883
873
  }
884
874
  }
875
+ /**
876
+ * Zooms to one section: its declared `bounds`, or the box around its seats
877
+ * and elements when it declares none. No-op unless `zoomable`, or for a
878
+ * section that is not rendered.
879
+ */
880
+ zoomToSection(sectionId) {
881
+ const layout = this.layout();
882
+ const placed = layout.sections.find((s) => s.section.id === sectionId);
883
+ if (placed) {
884
+ this.zoomToCanvasRect(placed.bounds, layout);
885
+ }
886
+ }
887
+ zoomToCanvasRect(rect, canvas) {
888
+ const measured = this.measure();
889
+ if (!measured || !this.zoomable() || !(canvas.width > 0 && canvas.height > 0)) {
890
+ return;
891
+ }
892
+ // The map's <svg> is the content's only child and sets its height; its
893
+ // width can stop short of the content's (`unitScale` caps it), so the
894
+ // drawn width comes from the height and the canvas's proportions.
895
+ const drawn = {
896
+ width: (measured.content.height * canvas.width) / canvas.height,
897
+ height: measured.content.height,
898
+ };
899
+ const next = viewportForRect(canvasRectToContent(rect, canvas, drawn), measured.content, measured.container, undefined, this.limits());
900
+ this.commit(next, true);
901
+ }
885
902
  stepZoom(direction) {
886
903
  const measured = this.measure();
887
904
  if (measured && this.zoomable()) {
@@ -1111,12 +1128,12 @@ class KerusiSeatmapComponent {
1111
1128
  return (this.map().sections ?? []).map((s) => s.id).filter((id) => !hidden.includes(id));
1112
1129
  }
1113
1130
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSeatmapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1114
- 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 }, seatTypeStyles: { classPropertyName: "seatTypeStyles", publicName: "seatTypeStyles", isSignal: true, isRequired: false, transformFunction: null }, seatStyle: { classPropertyName: "seatStyle", publicName: "seatStyle", 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" }, queries: [{ propertyName: "seatTemplate", first: true, predicate: KerusiSeatTemplateDirective, descendants: true, isSignal: true }], 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 [seatTypeStyles]=\"seatTypeStyles()\"\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 [seatTemplate]=\"seatTemplate()?.template\"\r\n [seatStyle]=\"seatStyle()\"\r\n [seatTypeStyles]=\"seatTypeStyles()\"\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", "seatTypeStyles", "seatStyle", "seatTemplate", "interactive", "unitScale"], outputs: ["seatActivate", "seatFocused", "seatKeydown"] }, { kind: "component", type: KerusiLegendComponent, selector: "kerusi-legend", inputs: ["legend", "locale", "colors", "typeColors", "seatTypeStyles", "showPrices", "hideUnusedTypes", "headingSeatTypes", "headingAvailability"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1131
+ 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 }, seatTypeStyles: { classPropertyName: "seatTypeStyles", publicName: "seatTypeStyles", isSignal: true, isRequired: false, transformFunction: null }, seatStyle: { classPropertyName: "seatStyle", publicName: "seatStyle", 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" }, queries: [{ propertyName: "seatTemplate", first: true, predicate: KerusiSeatTemplateDirective, descendants: true, isSignal: true }], 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\n class=\"kerusi-seatmap\"\n [class.kerusi-seatmap--maximized]=\"maximized()\"\n role=\"group\"\n [attr.aria-label]=\"mapLabel()\"\n [attr.dir]=\"isRtl() ? 'rtl' : null\"\n [attr.lang]=\"resolvedLocale()\"\n [style.background]=\"maximized() ? controlColors().maximizedBackground : null\"\n [style.color]=\"maximized() ? controlColors().maximizedColor : null\"\n (keydown)=\"onRootKeydown($event)\"\n>\n @if (zoomable() || maximizable()) {\n <!-- Only when a zoom or maximize feature is on, so a plain map's markup is\n exactly what it was before they existed. -->\n <div class=\"kerusi-seatmap__stage\">\n @if (showToolbar()) {\n <div\n class=\"kerusi-toolbar\"\n role=\"toolbar\"\n [attr.aria-label]=\"strings().toolbar\"\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\n >\n @if (zoomable() && showZoomControls()) {\n <button\n type=\"button\"\n class=\"kerusi-control kerusi-control--zoom-in\"\n [attr.aria-label]=\"strings().zoomIn\"\n [attr.aria-disabled]=\"atMaxZoom()\"\n [style.background]=\"controlColors().buttonBackground\"\n [style.color]=\"controlColors().buttonColor\"\n [style.border-color]=\"controlColors().buttonBorder\"\n (click)=\"zoomIn()\"\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M12 5v14M5 12h14\" />\n </svg>\n </button>\n <button\n type=\"button\"\n class=\"kerusi-control kerusi-control--zoom-out\"\n [attr.aria-label]=\"strings().zoomOut\"\n [attr.aria-disabled]=\"atFit()\"\n [style.background]=\"controlColors().buttonBackground\"\n [style.color]=\"controlColors().buttonColor\"\n [style.border-color]=\"controlColors().buttonBorder\"\n (click)=\"zoomOut()\"\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M5 12h14\" />\n </svg>\n </button>\n <button\n type=\"button\"\n class=\"kerusi-control kerusi-control--zoom-reset\"\n [attr.aria-label]=\"strings().zoomReset\"\n [attr.aria-disabled]=\"atFit()\"\n [style.background]=\"controlColors().buttonBackground\"\n [style.color]=\"controlColors().buttonColor\"\n [style.border-color]=\"controlColors().buttonBorder\"\n (click)=\"resetZoom()\"\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M3 12a9 9 0 1 0 3-6.7M3 4v5h5\" />\n </svg>\n </button>\n }\n @if (maximizable()) {\n <button\n type=\"button\"\n class=\"kerusi-control kerusi-control--maximize\"\n [attr.aria-label]=\"maximized() ? strings().restore : strings().maximize\"\n [style.background]=\"controlColors().buttonBackground\"\n [style.color]=\"controlColors().buttonColor\"\n [style.border-color]=\"controlColors().buttonBorder\"\n (click)=\"toggleMaximized()\"\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n @if (maximized()) {\n <path d=\"M9 4v5H4M15 4v5h5M15 20v-5h5M9 20v-5H4\" />\n } @else {\n <path d=\"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5\" />\n }\n </svg>\n </button>\n }\n </div>\n }\n\n @if (zoomable()) {\n <div\n #viewport\n class=\"kerusi-viewport\"\n role=\"group\"\n tabindex=\"0\"\n [attr.aria-label]=\"strings().viewport\"\n [style.touch-action]=\"touchAction()\"\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\n [style.--kerusi-canvas-aspect]=\"canvasAspect()\"\n >\n <div\n #content\n class=\"kerusi-viewport__content\"\n [class.kerusi-viewport__content--animated]=\"animated()\"\n [style.transform]=\"transform()\"\n >\n <ng-container [ngTemplateOutlet]=\"drawing\" />\n </div>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"drawing\" />\n }\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"drawing\" />\n }\n\n @if (showLegend()) {\n <kerusi-legend\n [legend]=\"renderMap().legend\"\n [locale]=\"resolvedLocale()\"\n [colors]=\"resolvedColors()\"\n [typeColors]=\"typeColors()\"\n [seatTypeStyles]=\"seatTypeStyles()\"\n [showPrices]=\"showLegendPrices()\"\n />\n }\n\n <p class=\"kerusi-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveMessage() }}</p>\n</div>\n\n<!-- The whole map: one <svg> on the document's canvas, a group per section (D1). -->\n<ng-template #drawing>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"kerusi-seatmap__svg\"\n role=\"presentation\"\n focusable=\"false\"\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\n [attr.viewBox]=\"viewBox()\"\n [style.max-width.px]=\"naturalWidth()\"\n preserveAspectRatio=\"xMidYMid meet\"\n >\n <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"layout().width\"\n [attr.height]=\"layout().height\"\n [style.fill]=\"backdrop()\"\n />\n <!-- A hidden section's group is simply absent; the canvas keeps its size,\n so its space stays empty (D4). -->\n @for (placed of layout().sections; track placed.section.id) {\n <g\n kerusi-section\n [layout]=\"placed\"\n [label]=\"labelFor(placed.section)\"\n [showLabel]=\"showSectionLabels()\"\n [selection]=\"selectionSet()\"\n [focusedSeatId]=\"focusedSeatIn(placed.section)\"\n [ariaLabels]=\"ariaLabels()\"\n [colors]=\"resolvedColors()\"\n [typeColors]=\"typeColors()\"\n [seatTemplate]=\"seatTemplate()?.template\"\n [seatStyle]=\"seatStyle()\"\n [seatTypeStyles]=\"seatTypeStyles()\"\n [interactive]=\"interactive()\"\n (seatActivate)=\"onSeatActivate($event)\"\n (seatFocused)=\"onSeatFocused($event)\"\n (seatKeydown)=\"onKeydown(placed.section, $event)\"\n ></g>\n }\n </svg>\n</ng-template>\n", styles: [":host{display:block}.kerusi-seatmap{display:flex;flex-direction:column;gap:1.25rem}.kerusi-seatmap__svg{display:block;width:100%;height:auto;max-width:100%}.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;container-type:size}.kerusi-seatmap--maximized .kerusi-viewport__content{width:min(100cqw,100cqh * var(--kerusi-canvas-aspect, 1))}\n"], dependencies: [{ kind: "component", type: KerusiSectionComponent, selector: "g[kerusi-section]", inputs: ["layout", "label", "showLabel", "selection", "focusedSeatId", "ariaLabels", "colors", "typeColors", "seatTypeStyles", "seatStyle", "seatTemplate", "interactive"], outputs: ["seatActivate", "seatFocused", "seatKeydown"] }, { kind: "component", type: KerusiLegendComponent, selector: "kerusi-legend", inputs: ["legend", "locale", "colors", "typeColors", "seatTypeStyles", "showPrices", "hideUnusedTypes", "headingSeatTypes", "headingAvailability"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1115
1132
  }
1116
1133
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSeatmapComponent, decorators: [{
1117
1134
  type: Component,
1118
- 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 [seatTypeStyles]=\"seatTypeStyles()\"\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 [seatTemplate]=\"seatTemplate()?.template\"\r\n [seatStyle]=\"seatStyle()\"\r\n [seatTypeStyles]=\"seatTypeStyles()\"\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"] }]
1119
- }], 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 }] }], seatTypeStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTypeStyles", required: false }] }], seatStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatStyle", required: false }] }], seatTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KerusiSeatTemplateDirective), { isSignal: true }] }], 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 }] }] } });
1135
+ args: [{ selector: 'kerusi-seatmap', changeDetection: ChangeDetectionStrategy.OnPush, imports: [KerusiSectionComponent, KerusiLegendComponent, NgTemplateOutlet], template: "<div\n class=\"kerusi-seatmap\"\n [class.kerusi-seatmap--maximized]=\"maximized()\"\n role=\"group\"\n [attr.aria-label]=\"mapLabel()\"\n [attr.dir]=\"isRtl() ? 'rtl' : null\"\n [attr.lang]=\"resolvedLocale()\"\n [style.background]=\"maximized() ? controlColors().maximizedBackground : null\"\n [style.color]=\"maximized() ? controlColors().maximizedColor : null\"\n (keydown)=\"onRootKeydown($event)\"\n>\n @if (zoomable() || maximizable()) {\n <!-- Only when a zoom or maximize feature is on, so a plain map's markup is\n exactly what it was before they existed. -->\n <div class=\"kerusi-seatmap__stage\">\n @if (showToolbar()) {\n <div\n class=\"kerusi-toolbar\"\n role=\"toolbar\"\n [attr.aria-label]=\"strings().toolbar\"\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\n >\n @if (zoomable() && showZoomControls()) {\n <button\n type=\"button\"\n class=\"kerusi-control kerusi-control--zoom-in\"\n [attr.aria-label]=\"strings().zoomIn\"\n [attr.aria-disabled]=\"atMaxZoom()\"\n [style.background]=\"controlColors().buttonBackground\"\n [style.color]=\"controlColors().buttonColor\"\n [style.border-color]=\"controlColors().buttonBorder\"\n (click)=\"zoomIn()\"\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M12 5v14M5 12h14\" />\n </svg>\n </button>\n <button\n type=\"button\"\n class=\"kerusi-control kerusi-control--zoom-out\"\n [attr.aria-label]=\"strings().zoomOut\"\n [attr.aria-disabled]=\"atFit()\"\n [style.background]=\"controlColors().buttonBackground\"\n [style.color]=\"controlColors().buttonColor\"\n [style.border-color]=\"controlColors().buttonBorder\"\n (click)=\"zoomOut()\"\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M5 12h14\" />\n </svg>\n </button>\n <button\n type=\"button\"\n class=\"kerusi-control kerusi-control--zoom-reset\"\n [attr.aria-label]=\"strings().zoomReset\"\n [attr.aria-disabled]=\"atFit()\"\n [style.background]=\"controlColors().buttonBackground\"\n [style.color]=\"controlColors().buttonColor\"\n [style.border-color]=\"controlColors().buttonBorder\"\n (click)=\"resetZoom()\"\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M3 12a9 9 0 1 0 3-6.7M3 4v5h5\" />\n </svg>\n </button>\n }\n @if (maximizable()) {\n <button\n type=\"button\"\n class=\"kerusi-control kerusi-control--maximize\"\n [attr.aria-label]=\"maximized() ? strings().restore : strings().maximize\"\n [style.background]=\"controlColors().buttonBackground\"\n [style.color]=\"controlColors().buttonColor\"\n [style.border-color]=\"controlColors().buttonBorder\"\n (click)=\"toggleMaximized()\"\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n @if (maximized()) {\n <path d=\"M9 4v5H4M15 4v5h5M15 20v-5h5M9 20v-5H4\" />\n } @else {\n <path d=\"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5\" />\n }\n </svg>\n </button>\n }\n </div>\n }\n\n @if (zoomable()) {\n <div\n #viewport\n class=\"kerusi-viewport\"\n role=\"group\"\n tabindex=\"0\"\n [attr.aria-label]=\"strings().viewport\"\n [style.touch-action]=\"touchAction()\"\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\n [style.--kerusi-canvas-aspect]=\"canvasAspect()\"\n >\n <div\n #content\n class=\"kerusi-viewport__content\"\n [class.kerusi-viewport__content--animated]=\"animated()\"\n [style.transform]=\"transform()\"\n >\n <ng-container [ngTemplateOutlet]=\"drawing\" />\n </div>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"drawing\" />\n }\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"drawing\" />\n }\n\n @if (showLegend()) {\n <kerusi-legend\n [legend]=\"renderMap().legend\"\n [locale]=\"resolvedLocale()\"\n [colors]=\"resolvedColors()\"\n [typeColors]=\"typeColors()\"\n [seatTypeStyles]=\"seatTypeStyles()\"\n [showPrices]=\"showLegendPrices()\"\n />\n }\n\n <p class=\"kerusi-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveMessage() }}</p>\n</div>\n\n<!-- The whole map: one <svg> on the document's canvas, a group per section (D1). -->\n<ng-template #drawing>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"kerusi-seatmap__svg\"\n role=\"presentation\"\n focusable=\"false\"\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\n [attr.viewBox]=\"viewBox()\"\n [style.max-width.px]=\"naturalWidth()\"\n preserveAspectRatio=\"xMidYMid meet\"\n >\n <rect\n x=\"0\"\n y=\"0\"\n [attr.width]=\"layout().width\"\n [attr.height]=\"layout().height\"\n [style.fill]=\"backdrop()\"\n />\n <!-- A hidden section's group is simply absent; the canvas keeps its size,\n so its space stays empty (D4). -->\n @for (placed of layout().sections; track placed.section.id) {\n <g\n kerusi-section\n [layout]=\"placed\"\n [label]=\"labelFor(placed.section)\"\n [showLabel]=\"showSectionLabels()\"\n [selection]=\"selectionSet()\"\n [focusedSeatId]=\"focusedSeatIn(placed.section)\"\n [ariaLabels]=\"ariaLabels()\"\n [colors]=\"resolvedColors()\"\n [typeColors]=\"typeColors()\"\n [seatTemplate]=\"seatTemplate()?.template\"\n [seatStyle]=\"seatStyle()\"\n [seatTypeStyles]=\"seatTypeStyles()\"\n [interactive]=\"interactive()\"\n (seatActivate)=\"onSeatActivate($event)\"\n (seatFocused)=\"onSeatFocused($event)\"\n (seatKeydown)=\"onKeydown(placed.section, $event)\"\n ></g>\n }\n </svg>\n</ng-template>\n", styles: [":host{display:block}.kerusi-seatmap{display:flex;flex-direction:column;gap:1.25rem}.kerusi-seatmap__svg{display:block;width:100%;height:auto;max-width:100%}.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;container-type:size}.kerusi-seatmap--maximized .kerusi-viewport__content{width:min(100cqw,100cqh * var(--kerusi-canvas-aspect, 1))}\n"] }]
1136
+ }], 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 }] }], seatTypeStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTypeStyles", required: false }] }], seatStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatStyle", required: false }] }], seatTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KerusiSeatTemplateDirective), { isSignal: true }] }], 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 }] }] } });
1120
1137
  function formatTotal(total, locale) {
1121
1138
  return total ? formatMoney(total, locale) : '';
1122
1139
  }