@kerusiweb/angular 2.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kerusiweb/angular",
3
- "version": "2.2.0",
3
+ "version": "3.0.0",
4
4
  "description": "An Angular seat-map component that renders Kerusi Seat Map & Availability Format documents as interactive, accessible inline SVG.",
5
5
  "keywords": [
6
6
  "angular",
@@ -20,14 +20,14 @@
20
20
  "author": "Arsyad \"Shad Ahm\" Ahmad",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/ShadAhm/kerusiweb.git",
23
+ "url": "git+https://github.com/ShadAhm/kerusiweb.git",
24
24
  "directory": "projects/angular"
25
25
  },
26
26
  "homepage": "https://github.com/ShadAhm/kerusiweb",
27
27
  "peerDependencies": {
28
28
  "@angular/common": "^22.1.0",
29
29
  "@angular/core": "^22.1.0",
30
- "@kerusiweb/core": "^2.2.0"
30
+ "@kerusiweb/core": "^3.0.0"
31
31
  },
32
32
  "dependencies": {
33
33
  "tslib": "^2.3.0"
@@ -1,7 +1,53 @@
1
1
  import * as _kerusiweb_core from '@kerusiweb/core';
2
- import { KerusiMap, KerusiState, KerusiSession, SeatAriaStrings, KerusiSeatmapColors, RenderSeat, KerusiControlStrings, DisallowedReason, KerusiViolation, KerusiViewport, RenderMap, SectionLayout, RenderSection, PlacedSeat, SeatOccupantVariant, PlacedElement, RenderLegendEntry, SequenceReader, HeldSeat, KerusiStateDelta, DeltaApplication } from '@kerusiweb/core';
2
+ import { KerusiSeatContext, KerusiMap, KerusiState, KerusiSession, SeatAriaStrings, KerusiSeatmapColors, SeatStyleFn, RenderSeat, KerusiControlStrings, DisallowedReason, KerusiViolation, KerusiViewport, RenderMap, MapLayout, RenderSection, PlacedSection, PlacedSeat, PlacedElement, RenderLegendEntry, SeatOccupantVariant, SequenceReader, HeldSeat, KerusiStateDelta, DeltaApplication } from '@kerusiweb/core';
3
3
  import * as _angular_core from '@angular/core';
4
- import { Signal } from '@angular/core';
4
+ import { TemplateRef, Signal } from '@angular/core';
5
+
6
+ /**
7
+ * What a `kerusiSeat` template is handed for each seat (`let-seat`): the seat,
8
+ * whether it is selected, the stock glyph core resolved for it, and the stock
9
+ * drawing itself.
10
+ */
11
+ interface SeatTemplateContext extends KerusiSeatContext {
12
+ /**
13
+ * The library's own drawing of this seat. Render it to keep the stock seat,
14
+ * or alongside your own marks to add to it:
15
+ *
16
+ * ```html
17
+ * <ng-container *ngTemplateOutlet="seat.defaultContent; context: { $implicit: seat }" />
18
+ * ```
19
+ */
20
+ defaultContent: TemplateRef<SeatTemplateOutletContext>;
21
+ }
22
+ /** The context object a seat template is stamped with. */
23
+ interface SeatTemplateOutletContext {
24
+ $implicit: SeatTemplateContext;
25
+ }
26
+ /**
27
+ * Marks an `<ng-template>` inside `<kerusi-seatmap>` as the drawing for every
28
+ * seat. The library keeps each seat's `<g>`, with its role, ARIA state, tab
29
+ * stop, rotation and event handlers, and stamps this template inside it.
30
+ *
31
+ * The template is declared outside any `<svg>`, so Angular cannot tell its
32
+ * elements are SVG: prefix each one with `svg:` (`<svg:path>`, `<svg:image>`),
33
+ * or it is created as an unknown HTML element and draws nothing.
34
+ *
35
+ * ```html
36
+ * <kerusi-seatmap [map]="map">
37
+ * <ng-template kerusiSeat let-seat>
38
+ * <ng-container *ngTemplateOutlet="seat.defaultContent; context: { $implicit: seat }" />
39
+ * <svg:circle [attr.cx]="seat.glyph.box.x" [attr.cy]="seat.glyph.box.y" r="3" />
40
+ * </ng-template>
41
+ * </kerusi-seatmap>
42
+ * ```
43
+ */
44
+ declare class KerusiSeatTemplateDirective {
45
+ readonly template: TemplateRef<SeatTemplateOutletContext>;
46
+ /** Types `let-seat` as a {@link SeatTemplateContext}. */
47
+ static ngTemplateContextGuard(_dir: KerusiSeatTemplateDirective, ctx: unknown): ctx is SeatTemplateOutletContext;
48
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<KerusiSeatTemplateDirective, never>;
49
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<KerusiSeatTemplateDirective, "ng-template[kerusiSeat]", never, {}, {}, never, never, true, never>;
50
+ }
5
51
 
6
52
  /** How a document with validation errors is handled. */
7
53
  type ValidationMode =
@@ -13,11 +59,8 @@ type ValidationMode =
13
59
  | 'off';
14
60
  /** Per-section rendering overrides, keyed by `Section.id`. */
15
61
  interface SectionRenderOptions {
62
+ /** Leaves the section out. Its space on the canvas stays empty (D4). */
16
63
  hidden?: boolean;
17
- /** Overrides `Section.aspectRatio`. */
18
- aspectRatio?: string;
19
- /** Overrides the seat size for this section only. */
20
- seatSize?: number;
21
64
  /** Overrides the localized `Section.label`. */
22
65
  label?: string;
23
66
  }
@@ -37,8 +80,10 @@ interface SeatDisallowed {
37
80
  * Renders a Kerusi seat map.
38
81
  *
39
82
  * Takes a `KerusiMap` and its `KerusiState` directly — no adapter, no
40
- * intermediate row model. Each `Section` becomes its own `<svg>` with its own
41
- * layout mode and proportions, seats take their color from the map's legend,
83
+ * intermediate row model. The map is one `<svg>` whose `viewBox` is the
84
+ * document's canvas (§4.5), each `Section` is a group on it, every seat is
85
+ * drawn exactly where and as large as the document says, seats take their
86
+ * color from the map's legend,
42
87
  * every seat is keyboard-reachable and announced with its type, price, status
43
88
  * and §4.3.4 accessibility properties, and selection is an immutable set of
44
89
  * seat ids the caller can two-way bind.
@@ -69,17 +114,34 @@ declare class KerusiSeatmapComponent {
69
114
  readonly colors: _angular_core.InputSignal<KerusiSeatmapColors>;
70
115
  /** Let an available seat take its `SeatType.color` (§4.7). */
71
116
  readonly typeColors: _angular_core.InputSignal<boolean>;
72
- /** Grid cell edge, in viewBox units. */
73
- readonly seatSize: _angular_core.InputSignal<number>;
74
- /** Gap between grid cells, in viewBox units. */
75
- readonly seatGap: _angular_core.InputSignal<number>;
76
- /** Freeform viewBox width; height follows the section's aspect ratio. */
77
- readonly freeformBasis: _angular_core.InputSignal<number>;
78
117
  /**
79
- * CSS pixels per viewBox unit. Caps each section at its natural size so a
80
- * narrow section and a wide one in the same map draw seats the same size.
118
+ * Styles for every seat of a type, keyed by `SeatType.id`: fill, label colour
119
+ * and classes. Never changes a selected or blocked seat's fill. Also shown in
120
+ * the legend.
121
+ */
122
+ readonly seatTypeStyles: _angular_core.InputSignal<Readonly<Record<string, _kerusiweb_core.KerusiSeatStyle>> | undefined>;
123
+ /**
124
+ * A style for one seat, laid over its `seatTypeStyles` entry; return nothing
125
+ * to leave a seat be. Never changes a selected or blocked seat's fill.
126
+ */
127
+ readonly seatStyle: _angular_core.InputSignal<SeatStyleFn | undefined>;
128
+ /**
129
+ * A `<ng-template kerusiSeat let-seat>` placed inside the map draws the inside
130
+ * of every seat in place of the stock glyph. See {@link KerusiSeatTemplateDirective}.
131
+ */
132
+ protected readonly seatTemplate: _angular_core.Signal<KerusiSeatTemplateDirective | undefined>;
133
+ /**
134
+ * CSS pixels per canvas unit, at most: the map never draws wider than
135
+ * `canvas.width × unitScale`, and scales down to a narrower container. The
136
+ * document fixes every seat's size and position (§4.5); this is the only
137
+ * sizing control. Default `1`.
81
138
  */
82
139
  readonly unitScale: _angular_core.InputSignal<number>;
140
+ /**
141
+ * Draws each section's label inside the map, at the top-left of its `bounds`.
142
+ * Only sections that declare `bounds` get one; every section's label is its
143
+ * group's accessible name either way. Default `true`.
144
+ */
83
145
  readonly showSectionLabels: _angular_core.InputSignal<boolean>;
84
146
  readonly showLegend: _angular_core.InputSignal<boolean>;
85
147
  readonly showLegendPrices: _angular_core.InputSignal<boolean>;
@@ -87,7 +149,7 @@ declare class KerusiSeatmapComponent {
87
149
  readonly sectionIds: _angular_core.InputSignal<readonly string[] | undefined>;
88
150
  readonly sectionOverrides: _angular_core.InputSignal<Readonly<Record<string, SectionRenderOptions>>>;
89
151
  /** Statuses in which a seat may be picked. Default: `['available']`. */
90
- readonly selectableStatuses: _angular_core.InputSignal<readonly ("held" | "booked" | "available" | "blocked")[]>;
152
+ readonly selectableStatuses: _angular_core.InputSignal<readonly ("held" | "booked" | "blocked" | "available")[]>;
91
153
  /** `auto` selects a seat's companion closure together (§4.6). */
92
154
  readonly companionMode: _angular_core.InputSignal<"auto" | "independent">;
93
155
  readonly maxSelection: _angular_core.InputSignal<number | undefined>;
@@ -169,8 +231,14 @@ declare class KerusiSeatmapComponent {
169
231
  protected readonly showToolbar: _angular_core.Signal<boolean>;
170
232
  protected readonly atMaxZoom: _angular_core.Signal<boolean>;
171
233
  protected readonly atFit: _angular_core.Signal<boolean>;
172
- /** Placed geometry per section, keyed by section id. */
173
- protected readonly layouts: _angular_core.Signal<ReadonlyMap<string, SectionLayout>>;
234
+ /** Every seat and element placed on the canvas, mirrored as a whole in RTL (D3). */
235
+ protected readonly layout: _angular_core.Signal<MapLayout>;
236
+ protected readonly viewBox: _angular_core.Signal<string>;
237
+ /** `unitScale` CSS pixels per canvas unit, at most. */
238
+ protected readonly naturalWidth: _angular_core.Signal<number>;
239
+ /** The canvas's width over its height, so a maximized map can fit the whole canvas. */
240
+ protected readonly canvasAspect: _angular_core.Signal<string>;
241
+ protected readonly backdrop: _angular_core.Signal<string>;
174
242
  /** Navigation graphs per section — arrows move within, Tab moves between. */
175
243
  private readonly graphs;
176
244
  protected readonly ariaLabels: _angular_core.Signal<ReadonlyMap<string, string>>;
@@ -179,17 +247,15 @@ declare class KerusiSeatmapComponent {
179
247
  protected readonly liveMessage: _angular_core.Signal<string>;
180
248
  /** The selection's seats and total, for callers that want it off the template. */
181
249
  readonly summary: _angular_core.Signal<_kerusiweb_core.SelectionSummary>;
182
- protected layoutFor(section: RenderSection): SectionLayout;
183
250
  protected labelFor(section: RenderSection): string | undefined;
184
- protected labelId(section: RenderSection): string;
185
251
  /** The seat holding this section's tab stop. */
186
252
  protected focusedSeatIn(section: RenderSection): string | null;
187
253
  protected onSeatActivate(seatId: string): void;
188
254
  protected onSeatFocused(seatId: string): void;
189
255
  /**
190
- * Arrow keys walk the navigation graph, which orders seats by `col` in grid
191
- * and mixed sections — §4.3.1's logical adjacency — so the aisle that is a
192
- * skipped column is stepped across rather than into.
256
+ * Arrow keys walk the navigation graph, which follows §4.3.5 adjacency:
257
+ * along a row by `col`, across an aisle (a skipped `col`) rather than into
258
+ * it, and between rows to the seat nearest on the canvas (D6).
193
259
  */
194
260
  protected onKeydown(section: RenderSection, event: KeyboardEvent): void;
195
261
  private setFocusedSeat;
@@ -210,8 +276,15 @@ declare class KerusiSeatmapComponent {
210
276
  zoomIn(): void;
211
277
  /** Zooms out one step about the centre. No-op unless `zoomable`. */
212
278
  zoomOut(): void;
213
- /** Back to fit-to-container: the map's original view. No-op unless `zoomable`. */
279
+ /** Back to fit-to-container: the whole canvas. No-op unless `zoomable`. */
214
280
  resetZoom(): void;
281
+ /**
282
+ * Zooms to one section: its declared `bounds`, or the box around its seats
283
+ * and elements when it declares none. No-op unless `zoomable`, or for a
284
+ * section that is not rendered.
285
+ */
286
+ zoomToSection(sectionId: string): void;
287
+ private zoomToCanvasRect;
215
288
  private stepZoom;
216
289
  /** Sizes in CSS pixels, read when needed: `offset*` ignores the zoom transform. */
217
290
  private measure;
@@ -247,23 +320,32 @@ declare class KerusiSeatmapComponent {
247
320
  private announce;
248
321
  private visibleSectionIds;
249
322
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<KerusiSeatmapComponent, never>;
250
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<KerusiSeatmapComponent, "kerusi-seatmap", never, { "map": { "alias": "map"; "required": true; "isSignal": true; }; "state": { "alias": "state"; "required": false; "isSignal": true; }; "session": { "alias": "session"; "required": false; "isSignal": true; }; "selection": { "alias": "selection"; "required": false; "isSignal": true; }; "locale": { "alias": "locale"; "required": false; "isSignal": true; }; "rtl": { "alias": "rtl"; "required": false; "isSignal": true; }; "ariaStrings": { "alias": "ariaStrings"; "required": false; "isSignal": true; }; "colors": { "alias": "colors"; "required": false; "isSignal": true; }; "typeColors": { "alias": "typeColors"; "required": false; "isSignal": true; }; "seatSize": { "alias": "seatSize"; "required": false; "isSignal": true; }; "seatGap": { "alias": "seatGap"; "required": false; "isSignal": true; }; "freeformBasis": { "alias": "freeformBasis"; "required": false; "isSignal": true; }; "unitScale": { "alias": "unitScale"; "required": false; "isSignal": true; }; "showSectionLabels": { "alias": "showSectionLabels"; "required": false; "isSignal": true; }; "showLegend": { "alias": "showLegend"; "required": false; "isSignal": true; }; "showLegendPrices": { "alias": "showLegendPrices"; "required": false; "isSignal": true; }; "sectionIds": { "alias": "sectionIds"; "required": false; "isSignal": true; }; "sectionOverrides": { "alias": "sectionOverrides"; "required": false; "isSignal": true; }; "selectableStatuses": { "alias": "selectableStatuses"; "required": false; "isSignal": true; }; "companionMode": { "alias": "companionMode"; "required": false; "isSignal": true; }; "maxSelection": { "alias": "maxSelection"; "required": false; "isSignal": true; }; "seatSelectable": { "alias": "seatSelectable"; "required": false; "isSignal": true; }; "interactive": { "alias": "interactive"; "required": false; "isSignal": true; }; "expireHolds": { "alias": "expireHolds"; "required": false; "isSignal": true; }; "expiryIntervalMs": { "alias": "expiryIntervalMs"; "required": false; "isSignal": true; }; "zoomable": { "alias": "zoomable"; "required": false; "isSignal": true; }; "maxZoom": { "alias": "maxZoom"; "required": false; "isSignal": true; }; "showZoomControls": { "alias": "showZoomControls"; "required": false; "isSignal": true; }; "maximizable": { "alias": "maximizable"; "required": false; "isSignal": true; }; "controlStrings": { "alias": "controlStrings"; "required": false; "isSignal": true; }; "validate": { "alias": "validate"; "required": false; "isSignal": true; }; }, { "selection": "selectionChange"; "seatSelect": "seatSelect"; "seatDeselect": "seatDeselect"; "seatDisallowed": "seatDisallowed"; "seatFocus": "seatFocus"; "validationIssues": "validationIssues"; "viewportChange": "viewportChange"; "maximizedChange": "maximizedChange"; }, never, never, true, never>;
323
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<KerusiSeatmapComponent, "kerusi-seatmap", never, { "map": { "alias": "map"; "required": true; "isSignal": true; }; "state": { "alias": "state"; "required": false; "isSignal": true; }; "session": { "alias": "session"; "required": false; "isSignal": true; }; "selection": { "alias": "selection"; "required": false; "isSignal": true; }; "locale": { "alias": "locale"; "required": false; "isSignal": true; }; "rtl": { "alias": "rtl"; "required": false; "isSignal": true; }; "ariaStrings": { "alias": "ariaStrings"; "required": false; "isSignal": true; }; "colors": { "alias": "colors"; "required": false; "isSignal": true; }; "typeColors": { "alias": "typeColors"; "required": false; "isSignal": true; }; "seatTypeStyles": { "alias": "seatTypeStyles"; "required": false; "isSignal": true; }; "seatStyle": { "alias": "seatStyle"; "required": false; "isSignal": true; }; "unitScale": { "alias": "unitScale"; "required": false; "isSignal": true; }; "showSectionLabels": { "alias": "showSectionLabels"; "required": false; "isSignal": true; }; "showLegend": { "alias": "showLegend"; "required": false; "isSignal": true; }; "showLegendPrices": { "alias": "showLegendPrices"; "required": false; "isSignal": true; }; "sectionIds": { "alias": "sectionIds"; "required": false; "isSignal": true; }; "sectionOverrides": { "alias": "sectionOverrides"; "required": false; "isSignal": true; }; "selectableStatuses": { "alias": "selectableStatuses"; "required": false; "isSignal": true; }; "companionMode": { "alias": "companionMode"; "required": false; "isSignal": true; }; "maxSelection": { "alias": "maxSelection"; "required": false; "isSignal": true; }; "seatSelectable": { "alias": "seatSelectable"; "required": false; "isSignal": true; }; "interactive": { "alias": "interactive"; "required": false; "isSignal": true; }; "expireHolds": { "alias": "expireHolds"; "required": false; "isSignal": true; }; "expiryIntervalMs": { "alias": "expiryIntervalMs"; "required": false; "isSignal": true; }; "zoomable": { "alias": "zoomable"; "required": false; "isSignal": true; }; "maxZoom": { "alias": "maxZoom"; "required": false; "isSignal": true; }; "showZoomControls": { "alias": "showZoomControls"; "required": false; "isSignal": true; }; "maximizable": { "alias": "maximizable"; "required": false; "isSignal": true; }; "controlStrings": { "alias": "controlStrings"; "required": false; "isSignal": true; }; "validate": { "alias": "validate"; "required": false; "isSignal": true; }; }, { "selection": "selectionChange"; "seatSelect": "seatSelect"; "seatDeselect": "seatDeselect"; "seatDisallowed": "seatDisallowed"; "seatFocus": "seatFocus"; "validationIssues": "validationIssues"; "viewportChange": "viewportChange"; "maximizedChange": "maximizedChange"; }, ["seatTemplate"], never, true, never>;
251
324
  }
252
325
 
253
326
  /**
254
- * Draws one Kerusi `Section` as a single `<svg>`.
327
+ * Draws one Kerusi `Section` as a `<g>` on the map's shared canvas (D1).
255
328
  *
256
- * One SVG per section — rather than section groups inside a shared canvas — is
257
- * what makes per-section `aspectRatio` and mixed grid/freeform modes work: each
258
- * gets its own `viewBox` and its own intrinsic proportions, and CSS is free to
259
- * stack, wrap or column them. It also puts the section heading in real DOM,
260
- * where a screen reader can use it as a landmark.
329
+ * Sections are zones on one canvas in rev 15 (§4.1), and two may sit side by
330
+ * side, so a section is a group inside the map's single `<svg>` rather than an
331
+ * SVG of its own. Use it on a `<g>` inside an `<svg>` whose `viewBox` is the
332
+ * canvas: `<g kerusi-section [layout]="placed"></g>`. Its template prefixes
333
+ * every element with `svg:`, because Angular cannot tell from a component's
334
+ * own template that it will be stamped into SVG.
261
335
  *
262
336
  * This component holds no state. It takes a placed layout and emits intent.
263
337
  */
264
338
  declare class KerusiSectionComponent {
265
- readonly section: _angular_core.InputSignal<RenderSection>;
266
- readonly layout: _angular_core.InputSignal<SectionLayout>;
339
+ /** The section with its seats and elements placed on the map's canvas. */
340
+ readonly layout: _angular_core.InputSignal<PlacedSection>;
341
+ /** The section's name: its accessible name, and its drawn label when one is shown. */
342
+ readonly label: _angular_core.InputSignal<string | undefined>;
343
+ /**
344
+ * Draw `label` inside the SVG, at the top-left of the section's bounds. Only
345
+ * a section that declares `bounds` gets one (D5); otherwise the label is the
346
+ * group's accessible name alone.
347
+ */
348
+ readonly showLabel: _angular_core.InputSignal<boolean>;
267
349
  /** Currently selected seat ids. */
268
350
  readonly selection: _angular_core.InputSignal<ReadonlySet<string>>;
269
351
  /** The seat holding the section's single tab stop, if any. */
@@ -273,80 +355,32 @@ declare class KerusiSectionComponent {
273
355
  readonly colors: _angular_core.InputSignal<Required<KerusiSeatmapColors>>;
274
356
  /** Whether an available seat may take its `SeatType.color` (§4.7). */
275
357
  readonly typeColors: _angular_core.InputSignal<boolean>;
358
+ /** Styles for every seat of a type, keyed by `SeatType.id`. */
359
+ readonly seatTypeStyles: _angular_core.InputSignal<Readonly<Record<string, _kerusiweb_core.KerusiSeatStyle>> | undefined>;
360
+ /** A style for one seat, laid over its type's. */
361
+ readonly seatStyle: _angular_core.InputSignal<SeatStyleFn | undefined>;
362
+ /** Draws the inside of each seat in place of the stock glyph. */
363
+ readonly seatTemplate: _angular_core.InputSignal<TemplateRef<SeatTemplateOutletContext> | undefined>;
276
364
  /** When false, seats render but do not respond to pointer or keyboard. */
277
365
  readonly interactive: _angular_core.InputSignal<boolean>;
278
- /**
279
- * CSS pixels per viewBox unit, at the section's natural size.
280
- *
281
- * Without a cap every section stretches to the full container width, so in a
282
- * four-tier theatre the narrow box row would draw seats several times the
283
- * size of the orchestra's. Capping each section at its own intrinsic width
284
- * keeps a seat the same size everywhere; a section too wide to fit still
285
- * scales down to the container.
286
- */
287
- readonly unitScale: _angular_core.InputSignal<number>;
288
366
  readonly seatActivate: _angular_core.OutputEmitterRef<string>;
289
367
  readonly seatFocused: _angular_core.OutputEmitterRef<string>;
290
368
  readonly seatKeydown: _angular_core.OutputEmitterRef<KeyboardEvent>;
291
- protected readonly viewBox: _angular_core.Signal<string>;
292
- /** The section's natural width in CSS pixels; it never draws wider. */
293
- protected readonly naturalWidth: _angular_core.Signal<number>;
294
- protected isSelected(placed: PlacedSeat): boolean;
295
- protected fill(placed: PlacedSeat): string;
296
- protected textFill(placed: PlacedSeat): string;
297
- protected backdrop(): string;
298
- /** The seat's outline: square-ish front, tapered back — shape carries facing. */
299
- protected bodyPath(placed: PlacedSeat): string;
300
- /** `null` for an available or blocked seat — nothing to mark. */
301
- protected occupantVariant(placed: PlacedSeat): SeatOccupantVariant | null;
302
- /**
303
- * The box a seat's decorative marks are drawn to: the seat's own, or — when
304
- * it is selected — the core plate's, which is inset by the frame width.
305
- *
306
- * Everything over a selected seat sits on the core, so it all has to be
307
- * measured from the core. Drawing to the seat's box instead leaves the
308
- * occupant's hips crossing the frame, where a figure tinted `selectedBg`
309
- * lands on a frame filled `selectedBg` and reads as a hard nub. Since the
310
- * core is an exact scaled copy of the body (see {@link seatSelectedFrame}),
311
- * routing through here reproduces every clearance the mark had against the
312
- * body, scaled — so the frame width stays free to change.
313
- */
314
- private markBox;
315
- /** The bright plate inside a selected seat's frame. */
316
- protected corePath(placed: PlacedSeat): string;
317
- protected coreFill(): string;
318
- /** The dimming wash over a held or booked seat's type colour. */
319
- protected washFill(placed: PlacedSeat): string;
320
- /** The occupant figure for a marked seat, sized to whatever it sits on. */
321
- protected occupantPath(placed: PlacedSeat): string;
322
- protected occupantStroke(placed: PlacedSeat): number;
323
- protected wheelchairCx(placed: PlacedSeat): number;
324
- protected wheelchairCy(placed: PlacedSeat): number;
325
- protected wheelchairRadius(placed: PlacedSeat): number;
369
+ /** Whether the label is drawn: asked for, present, and with declared bounds to sit in. */
370
+ protected readonly drawsLabel: _angular_core.Signal<boolean>;
371
+ protected readonly labelFill: _angular_core.Signal<string>;
326
372
  /**
327
- * A core-coloured outline behind a selected seat's number, painted under the
328
- * glyphs via `paint-order`.
329
- *
330
- * The label is centred on the occupant's shoulder line, so without this it is
331
- * `selectedBg` over a `selectedBg` silhouette — the figure, not the plate, is
332
- * what it actually has to contrast against. The halo also rescues a label too
333
- * long for the core, which would otherwise run onto the frame and vanish into
334
- * it.
335
- *
336
- * Bound here rather than in the stylesheet on purpose: a CSS
337
- * `var(--kerusi-selected-fg, …)` rule would carry the library default as its
338
- * fallback and silently skip the `[colors]` input tier, which only the
339
- * TypeScript side can resolve.
373
+ * Every seat with its glyph resolved, once per change of layout, selection or
374
+ * theme rather than once per binding per change detection.
340
375
  */
341
- protected labelHalo(placed: PlacedSeat): string;
342
- protected labelHaloWidth(placed: PlacedSeat): number;
343
- /** Solid for selected/booked; `none` for held, which draws hollow instead. */
344
- protected occupantFill(variant: SeatOccupantVariant): string;
345
- protected occupantStrokeColor(variant: SeatOccupantVariant): string;
376
+ protected readonly seats: _angular_core.Signal<{
377
+ placed: PlacedSeat;
378
+ selected: boolean;
379
+ glyph: _kerusiweb_core.SeatGlyph;
380
+ }[]>;
346
381
  /** The section's single tab stop; every other seat is reachable by arrow key. */
347
382
  protected tabIndex(placed: PlacedSeat): number;
348
383
  protected ariaLabel(placed: PlacedSeat): string;
349
- protected seatClasses(placed: PlacedSeat): string;
350
384
  /** SVG rotate() about the item's center; null when it does not rotate. */
351
385
  protected transform(item: PlacedSeat | PlacedElement): string | null;
352
386
  /** Cancels the parent <g>'s rotation so the label stays upright and stays put. */
@@ -365,7 +399,7 @@ declare class KerusiSectionComponent {
365
399
  protected onSeatFocus(placed: PlacedSeat): void;
366
400
  protected onKeydown(event: KeyboardEvent): void;
367
401
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<KerusiSectionComponent, never>;
368
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<KerusiSectionComponent, "kerusi-section", never, { "section": { "alias": "section"; "required": true; "isSignal": true; }; "layout": { "alias": "layout"; "required": true; "isSignal": true; }; "selection": { "alias": "selection"; "required": false; "isSignal": true; }; "focusedSeatId": { "alias": "focusedSeatId"; "required": false; "isSignal": true; }; "ariaLabels": { "alias": "ariaLabels"; "required": false; "isSignal": true; }; "colors": { "alias": "colors"; "required": false; "isSignal": true; }; "typeColors": { "alias": "typeColors"; "required": false; "isSignal": true; }; "interactive": { "alias": "interactive"; "required": false; "isSignal": true; }; "unitScale": { "alias": "unitScale"; "required": false; "isSignal": true; }; }, { "seatActivate": "seatActivate"; "seatFocused": "seatFocused"; "seatKeydown": "seatKeydown"; }, never, never, true, never>;
402
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<KerusiSectionComponent, "g[kerusi-section]", never, { "layout": { "alias": "layout"; "required": true; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "showLabel": { "alias": "showLabel"; "required": false; "isSignal": true; }; "selection": { "alias": "selection"; "required": false; "isSignal": true; }; "focusedSeatId": { "alias": "focusedSeatId"; "required": false; "isSignal": true; }; "ariaLabels": { "alias": "ariaLabels"; "required": false; "isSignal": true; }; "colors": { "alias": "colors"; "required": false; "isSignal": true; }; "typeColors": { "alias": "typeColors"; "required": false; "isSignal": true; }; "seatTypeStyles": { "alias": "seatTypeStyles"; "required": false; "isSignal": true; }; "seatStyle": { "alias": "seatStyle"; "required": false; "isSignal": true; }; "seatTemplate": { "alias": "seatTemplate"; "required": false; "isSignal": true; }; "interactive": { "alias": "interactive"; "required": false; "isSignal": true; }; }, { "seatActivate": "seatActivate"; "seatFocused": "seatFocused"; "seatKeydown": "seatKeydown"; }, never, never, true, never>;
369
403
  }
370
404
 
371
405
  /** Which marks, beyond the body fill, a status swatch draws. */
@@ -398,6 +432,8 @@ declare class KerusiLegendComponent {
398
432
  readonly colors: _angular_core.InputSignal<Required<KerusiSeatmapColors>>;
399
433
  /** Whether an available seat takes its type color — mirrors the seatmap input. */
400
434
  readonly typeColors: _angular_core.InputSignal<boolean>;
435
+ /** Mirrors the seatmap input: a type's style is what its swatch shows. */
436
+ readonly seatTypeStyles: _angular_core.InputSignal<Readonly<Record<string, _kerusiweb_core.KerusiSeatStyle>> | undefined>;
401
437
  readonly showPrices: _angular_core.InputSignal<boolean>;
402
438
  /** Hide types no seat in the rendered map actually uses. */
403
439
  readonly hideUnusedTypes: _angular_core.InputSignal<boolean>;
@@ -406,6 +442,8 @@ declare class KerusiLegendComponent {
406
442
  protected readonly types: _angular_core.Signal<RenderLegendEntry[]>;
407
443
  protected readonly statuses: _angular_core.Signal<StatusEntry[]>;
408
444
  protected swatch(entry: RenderLegendEntry): string;
445
+ /** The swatch outline for a type styled with its own shape; `undefined` keeps the square. */
446
+ protected typeShape(entry: RenderLegendEntry): string | undefined;
409
447
  protected swatchText(entry: RenderLegendEntry): string;
410
448
  /** The swatch's viewBox edge; the seat glyph is drawn to fill it. */
411
449
  protected readonly glyphSize = 16;
@@ -429,7 +467,7 @@ declare class KerusiLegendComponent {
429
467
  protected coreFill(): string;
430
468
  protected price(entry: RenderLegendEntry): string;
431
469
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<KerusiLegendComponent, never>;
432
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<KerusiLegendComponent, "kerusi-legend", never, { "legend": { "alias": "legend"; "required": true; "isSignal": true; }; "locale": { "alias": "locale"; "required": false; "isSignal": true; }; "colors": { "alias": "colors"; "required": false; "isSignal": true; }; "typeColors": { "alias": "typeColors"; "required": false; "isSignal": true; }; "showPrices": { "alias": "showPrices"; "required": false; "isSignal": true; }; "hideUnusedTypes": { "alias": "hideUnusedTypes"; "required": false; "isSignal": true; }; "headingSeatTypes": { "alias": "headingSeatTypes"; "required": false; "isSignal": true; }; "headingAvailability": { "alias": "headingAvailability"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
470
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<KerusiLegendComponent, "kerusi-legend", never, { "legend": { "alias": "legend"; "required": true; "isSignal": true; }; "locale": { "alias": "locale"; "required": false; "isSignal": true; }; "colors": { "alias": "colors"; "required": false; "isSignal": true; }; "typeColors": { "alias": "typeColors"; "required": false; "isSignal": true; }; "seatTypeStyles": { "alias": "seatTypeStyles"; "required": false; "isSignal": true; }; "showPrices": { "alias": "showPrices"; "required": false; "isSignal": true; }; "hideUnusedTypes": { "alias": "hideUnusedTypes"; "required": false; "isSignal": true; }; "headingSeatTypes": { "alias": "headingSeatTypes"; "required": false; "isSignal": true; }; "headingAvailability": { "alias": "headingAvailability"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
433
471
  }
434
472
 
435
473
  /**
@@ -478,5 +516,5 @@ declare class KerusiStateStore {
478
516
  startExpiryTicker(intervalMs?: number): () => void;
479
517
  }
480
518
 
481
- export { KerusiLegendComponent, KerusiSeatmapComponent, KerusiSectionComponent, KerusiStateStore };
482
- export type { SeatDisallowed, SeatInteraction, SectionRenderOptions, ValidationMode };
519
+ export { KerusiLegendComponent, KerusiSeatTemplateDirective, KerusiSeatmapComponent, KerusiSectionComponent, KerusiStateStore };
520
+ export type { SeatDisallowed, SeatInteraction, SeatTemplateContext, SeatTemplateOutletContext, SectionRenderOptions, ValidationMode };