@kerusiweb/angular 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ npm install @kerusiweb/core @kerusiweb/angular
9
9
 
10
10
  Requires Angular 22+. Standalone, signal-based, zoneless-friendly.
11
11
 
12
- This package is the Angular binding only — the components and the signal-backed
12
+ This package is the Angular binding only: the components and the signal-backed
13
13
  `KerusiStateStore`. The document types, the conformance validator, the render
14
14
  model and its geometry live in its peer
15
15
  [`@kerusiweb/core`](https://www.npmjs.com/package/@kerusiweb/core), which is
@@ -47,14 +47,14 @@ import { KerusiSeatmapComponent } from '@kerusiweb/angular';
47
47
 
48
48
  **Selection**
49
49
 
50
- | Input | Type | Default | |
51
- | -------------------- | ------------------------------- | --------------- | ------------------------------------------------------------------------------- |
52
- | `selection` | `readonly string[]` | `[]` | Selected seat ids. Two-way — `[(selection)]` — or read-only with `[selection]`. |
53
- | `selectableStatuses` | `SeatRenderStatus[]` | `['available']` | Which statuses a seat may be picked in. |
54
- | `companionMode` | `'auto' \| 'independent'` | `'auto'` | `auto` selects a seat's whole companion closure together (§4.6). |
55
- | `maxSelection` | `number` | — | Cap on selected seats. Counts a companion closure as its full size. |
56
- | `seatSelectable` | `(seat: RenderSeat) => boolean` | — | A final say, applied after the status test. |
57
- | `interactive` | `boolean` | `true` | `false` renders read-only. |
50
+ | Input | Type | Default | |
51
+ | -------------------- | ------------------------------- | --------------- | -------------------------------------------------------------------------- |
52
+ | `selection` | `readonly string[]` | `[]` | Selected seat ids. Two-way (`[(selection)]`) or read-only (`[selection]`). |
53
+ | `selectableStatuses` | `SeatRenderStatus[]` | `['available']` | Which statuses a seat may be picked in. |
54
+ | `companionMode` | `'auto' \| 'independent'` | `'auto'` | `auto` selects a seat's whole companion closure together (§4.6). |
55
+ | `maxSelection` | `number` | — | Cap on selected seats. Counts a companion closure as its full size. |
56
+ | `seatSelectable` | `(seat: RenderSeat) => boolean` | — | A final say, applied after the status test. |
57
+ | `interactive` | `boolean` | `true` | `false` renders read-only. |
58
58
 
59
59
  **Appearance**
60
60
 
@@ -87,6 +87,16 @@ import { KerusiSeatmapComponent } from '@kerusiweb/angular';
87
87
  | `expireHolds` | `boolean` | `false` | Revert a lapsed `holdExpires` to available on a ticker. |
88
88
  | `expiryIntervalMs` | `number` | `1000` | |
89
89
 
90
+ **Zoom, pan and maximize** (both off by default; see [Zoom, pan and maximize](#zoom-pan-and-maximize))
91
+
92
+ | Input | Type | Default | |
93
+ | ------------------ | ------------------------------- | ------- | -------------------------------------------------------------------------------------- |
94
+ | `zoomable` | `boolean` | `false` | Pinch, Ctrl/Cmd + wheel and drag to zoom and pan; `+` `-` `0` keys; on-screen buttons. |
95
+ | `maxZoom` | `number` | `4` | The zoom ceiling, relative to fit. The floor is always fit. |
96
+ | `showZoomControls` | `boolean` | `true` | The built-in zoom buttons. Turn off to supply your own. |
97
+ | `maximizable` | `boolean` | `false` | A button that fills the screen with the map. |
98
+ | `controlStrings` | `Partial<KerusiControlStrings>` | English | The names of the zoom and maximize buttons, for translation. |
99
+
90
100
  ### Outputs
91
101
 
92
102
  | Output | Payload | |
@@ -96,22 +106,24 @@ import { KerusiSeatmapComponent } from '@kerusiweb/angular';
96
106
  | `seatDisallowed` | `SeatDisallowed` | `reason` is `booked`, `held`, `blocked`, `not-selectable`, `max-selection` or `companion-unavailable`. |
97
107
  | `seatFocus` | `RenderSeat` | |
98
108
  | `validationIssues` | `readonly KerusiViolation[]` | Emitted whenever the documents change. |
109
+ | `viewportChange` | `KerusiViewport` | `{ zoom, x, y }`, whenever the zoom or pan changes. Only with `zoomable`. |
110
+ | `maximizedChange` | `boolean` | When the map is maximized or restored. Only with `maximizable`. |
99
111
 
100
112
  ### The seat glyph
101
113
 
102
114
  **Colour means seat type; shape means status.** A seat's fill is always its
103
- `SeatType.color`, or the theme's `availableBg` when the type has none — it
115
+ `SeatType.color`, or the theme's `availableBg` when the type has none. It
104
116
  does not change when the seat is held or booked. Overloading fill with both
105
117
  type and status hid the type colour on exactly the seats a busy map has most
106
118
  of; see the doc comment on `seatFill` for the full argument. `selected` is the
107
- one deliberate exception: it still owns the seat's colour outright, so your own picks
119
+ one exception: it still owns the seat's colour outright, so your own picks
108
120
  are never ambiguous, and a purple fill on its own would in any case fail WCAG
109
- 1.4.1 (Use of Colour) for anyone who cannot distinguish it — hence the shape
121
+ 1.4.1 (Use of Colour) for anyone who cannot distinguish it, hence the shape
110
122
  cues below.
111
123
 
112
124
  The seat body itself carries orientation: square-ish at the front, tapered at
113
125
  the back, so which way a seat faces is legible from its outline alone.
114
- `Seat.rotation` turns the whole group, and the taper turns with it — no
126
+ `Seat.rotation` turns the whole group, and the taper turns with it; no
115
127
  separate frame is drawn on top. This is what makes a fanned lecture theatre or
116
128
  a stadium stand read at a glance. The seat number counter-rotates and stays
117
129
  upright regardless.
@@ -122,30 +134,29 @@ Status is read from two marks layered over the body, never from its colour:
122
134
  | --------- | ---------------------------------------------- | ---------- | ---------------------------------------------------- |
123
135
  | Available | type colour | — | — |
124
136
  | Selected | `selectedBg` **frame** + `selectedFg` **core** | — | solid, tinted `selectedBg` |
125
- | Held | type colour | `heldBg` | **hollow**, stroked `heldFg` — a hold is provisional |
126
- | Booked | type colour | `bookedBg` | solid, tinted `bookedFg` — settled, someone else's |
137
+ | Held | type colour | `heldBg` | **hollow**, stroked `heldFg` (a hold is provisional) |
138
+ | Booked | type colour | `bookedBg` | solid, tinted `bookedFg` (settled, someone else's) |
127
139
  | Blocked | `blockedBg` | — | — (withheld by the venue; no one is there) |
128
140
 
129
- Solid vs. hollow is the real distinction — settled vs. still in progress — and
141
+ Solid vs. hollow is the main distinction (settled vs. still in progress), and
130
142
  every figure leans with the seat rather than staying upright, so it doubles as
131
143
  an orientation cue too.
132
144
 
133
- Selection is the only state drawn from **two** tones rather than one, and that
134
- is deliberate. `selectedBg` frames the seat; `selectedFg` fills a core inside
145
+ Selection is the only state drawn from **two** tones rather than one. `selectedBg` frames the seat; `selectedFg` fills a core inside
135
146
  that frame and is what the number and the figure are read against. Because the
136
147
  treatment always holds a light tone and a dark one at once, one of the two
137
- separates from the page whichever way you have themed it — so the library never
148
+ separates from the page whichever way you have themed it, so the library never
138
149
  has to detect a colour scheme, and there is no `prefers-color-scheme` default to
139
150
  fight with your own. An earlier revision had this inverted, a near-white rim
140
151
  around a mid-purple middle, which put the low-contrast tone on the outside
141
152
  boundary: a selected seat dissolved into a light page and sank into a dark one.
142
153
 
143
- Both tokens therefore carry real visual weight — override them as a pair. Swapping
154
+ Both tokens therefore carry visual weight, so override them as a pair. Swapping
144
155
  the two gives the inverse treatment, a light frame around a dark core, which works
145
156
  just as well.
146
157
 
147
158
  A theme that overrides `heldFg`/`bookedFg` owns the contrast of that mark
148
- against whatever `SeatType.color` the document supplies — the library cannot
159
+ against whatever `SeatType.color` the document supplies. The library cannot
149
160
  know the pairing in advance, since the figure tint is fixed while the type
150
161
  colour underneath it is not. The shipped defaults are light marks over a
151
162
  darkened wash, which reads across the tier colours in the demo fixtures.
@@ -154,7 +165,7 @@ The geometry is exported, if you want to draw a matching seat elsewhere:
154
165
  `seatBodyPath`, `seatSelectedFrame`, `seatOccupantPath`, `seatOccupantStroke`.
155
166
  (`seatRingStroke` is the old name for `seatSelectedFrame` and still resolves to
156
167
  it.) A seat is always square, which makes `seatBodyPath`'s `inset` an exact
157
- scaled copy about the centre — that is how the selected core is derived, and why
168
+ scaled copy about the centre. That is how the selected core is derived, and why
158
169
  marks drawn to the core's box keep every clearance they had against the body.
159
170
 
160
171
  ### Theming
@@ -162,13 +173,13 @@ marks drawn to the core's box keep every clearance they had against the body.
162
173
  Colors resolve through three tiers, highest first:
163
174
 
164
175
  1. A `--kerusi-*` custom property in **your** stylesheet.
165
- 2. The **`[colors]` input** — a partial `KerusiSeatmapColors`, merged over the defaults.
176
+ 2. The **`[colors]` input**: a partial `KerusiSeatmapColors`, merged over the defaults.
166
177
  3. The library default.
167
178
 
168
179
  The library never writes a `--kerusi-*` property onto its own host, which is
169
180
  what keeps that order true: every fill is emitted as
170
181
  `var(--kerusi-selected-bg, <the resolved input value>)`. Use whichever tier
171
- fits — the input for values known at build time, CSS for anything that has to
182
+ fits: the input for values known at build time, CSS for anything that has to
172
183
  respond to a media query or a theme class.
173
184
 
174
185
  ```css
@@ -199,15 +210,15 @@ Every key of `KerusiSeatmapColors` has a property, kebab-cased:
199
210
  | `focusRing` | `--kerusi-focus-ring` |
200
211
  | `backdrop` | `--kerusi-backdrop` |
201
212
 
202
- A `SeatType.color` from the document is deliberately **not** themable — it is
213
+ A `SeatType.color` from the document is **not** themable, because it is
203
214
  the map's own value under §4.7. Turn it off wholesale with
204
215
  `[typeColors]="false"` if you want the theme to own every fill.
205
216
 
206
217
  Overriding `selectedBg` does not cost you the selected cue: the core and the
207
- figure are shape, not color. Do set `selectedFg` alongside it, though — it is the
218
+ figure are shape, not color. Do set `selectedFg` alongside it, though. It is the
208
219
  core's fill, so it is most of a selected seat's area rather than just a label
209
220
  tint, and the two are read against each other. `heldBg`/`bookedBg` are the wash
210
- drawn over a taken seat's type colour, not the seat's own fill — see
221
+ drawn over a taken seat's type colour, not the seat's own fill. See
211
222
  [The seat glyph](#the-seat-glyph) for the full state table.
212
223
 
213
224
  If you need the pre-1.1 flat selected seat back, the core is its own node:
@@ -239,10 +250,82 @@ For anything the palette does not cover, each seat group carries class hooks:
239
250
  Inside a seat: `.kerusi-seat__box`, `__wash`, `__core`, `__occupant`
240
251
  (`__occupant--selected` / `--held` / `--booked`), `__label`, `__wheelchair`.
241
252
 
253
+ ### Zoom, pan and maximize
254
+
255
+ Two features, both **off by default**: with them off the markup and behaviour are
256
+ exactly what they were. They are independent; turn on either or both.
257
+
258
+ On a phone a map of several hundred seats is drawn at fit-to-width, so a seat is
259
+ too small to tap. `[zoomable]="true"` lets the reader zoom in and pan; `[maximizable]="true"` adds a button
260
+ that fills the screen.
261
+
262
+ **Zoom and pan.**
263
+
264
+ - **Pinch** with two fingers on a touch screen. **Ctrl/Cmd + wheel**, or a
265
+ trackpad pinch (Safari's included), on desktop. A plain wheel is left alone, so
266
+ the page still scrolls.
267
+ - **Drag** with one finger or the primary mouse button to pan.
268
+ - **A tap still selects**, at any zoom level. A touch or click that moves less
269
+ than 8 CSS pixels is a tap, and reaches the seat exactly as it does without
270
+ `zoomable`. Anything that moves further is a drag, and a drag never selects the
271
+ seat under it.
272
+ - **Limits.** The floor is fit-to-container, the ceiling is `maxZoom` (default
273
+ `4`), and panning stays inside the map. A map smaller than its container, which
274
+ happens when it is maximized on a large screen, is centred.
275
+ - **Gestures and the page.** `touch-action` is `pan-y` at fit, so a one-finger
276
+ vertical swipe still scrolls the page while a pinch is the map's; once zoomed in
277
+ or maximized it is `none`. With `zoomable` off, no `touch-action` is set.
278
+ - **Reduced motion.** The short transition on the zoom buttons, the keys and
279
+ reset is dropped under `prefers-reduced-motion: reduce`. A finger or wheel moves
280
+ the map directly and never eases.
281
+
282
+ | Key | |
283
+ | --------------- | ------------------------------------------------------------ |
284
+ | `+` / `=`, `-` | Zoom in / out, from anywhere inside the map |
285
+ | `0` | Reset to fit |
286
+ | `←` `→` `↑` `↓` | Pan, when the zoomable viewport itself has focus (Tab to it) |
287
+
288
+ On a seat, the arrow keys keep their usual job of moving seat focus, and a seat
289
+ that takes keyboard focus is scrolled into view. Ctrl, Cmd and Alt combinations
290
+ are left to the browser, and keys typed into a field you put inside the map are
291
+ ignored.
292
+
293
+ **Controls.** The zoom buttons (zoom in, zoom out, reset to fit) are real
294
+ `<button>`s with accessible names. At a limit they set `aria-disabled` rather than
295
+ `disabled`, so keyboard focus is not thrown away mid-press. Their names come from
296
+ `controlStrings`, for translation. They use the theme's existing tokens
297
+ (`--kerusi-element-bg`, `--kerusi-element-fg`, `--kerusi-element-muted-fg`,
298
+ `--kerusi-focus-ring`), so there is nothing new to theme.
299
+
300
+ To put the controls somewhere else, turn the built-in ones off
301
+ (`[showZoomControls]="false"`) and drive the map yourself:
302
+
303
+ ```ts
304
+ readonly map = viewChild.required(KerusiSeatmapComponent);
305
+ // this.map().zoomIn(); this.map().zoomOut(); this.map().resetZoom(); this.map().toggleMaximized();
306
+ // this.map().viewport() -> { zoom, x, y }
307
+ // this.map().maximized() -> boolean
308
+ ```
309
+
310
+ `viewportChange` reports `{ zoom, x, y }` whenever the view changes, for a host that wants
311
+ to display or persist it. (Restoring a saved view is not supported.)
312
+
313
+ **Maximize.** The button fills the screen with the map and its legend, freezes the
314
+ page behind it, and swaps to an "Exit full screen" button. The layout is a fixed
315
+ overlay, which is all iPhone Safari offers; where the browser has the Fullscreen
316
+ API it is requested on top, and leaving it with the browser's own Esc leaves the
317
+ map's maximized state too. `Escape` from the map also leaves, except that on a
318
+ seat with a non-empty selection the first `Escape` still clears the selection, as
319
+ it always has. The backdrop is `--kerusi-backdrop`, or the page's own canvas colour
320
+ while that is transparent; `--kerusi-maximized-z-index` (default `1000`) sets the
321
+ stacking. It is not a modal dialog: focus is not trapped, and an ancestor with a
322
+ `transform` or `filter` would anchor the fixed layout to itself instead of the
323
+ screen.
324
+
242
325
  ### Keyboard
243
326
 
244
327
  Tab moves between sections; each section keeps its own tab stop. Within a
245
- section, arrow keys follow the §4.3.1 `col` order — so an aisle, which is a
328
+ section, arrow keys follow the §4.3.1 `col` order, so an aisle, which is a
246
329
  column no seat occupies, is stepped across rather than into.
247
330
 
248
331
  | Key | |
@@ -259,6 +342,8 @@ Each seat is a `role="button"` announcing its position, type, price, status,
259
342
  every `Seat.accessibility` property and its attributes. A polite live region
260
343
  reports the running selection and any disallowed reason.
261
344
 
345
+ With `zoomable` on there are a few more keys: see [Zoom, pan and maximize](#zoom-pan-and-maximize).
346
+
262
347
  ---
263
348
 
264
349
  ## `<kerusi-legend>`
@@ -270,7 +355,7 @@ Rendered inline by `[showLegend]="true"`, or placed anywhere yourself:
270
355
  ```
271
356
 
272
357
  It resolves swatches through the same path as the seat fills, so the two cannot
273
- drift apart. Every availability swatch — not just **Selected** — draws the
358
+ drift apart. Every availability swatch, not just **Selected**, draws the
274
359
  seat glyph itself (wash, ring, occupant, as it applies) rather than a flat
275
360
  colour, because shape is the cue the map uses for status.
276
361
 
@@ -278,7 +363,7 @@ colour, because shape is the cue the map uses for status.
278
363
 
279
364
  ## Working with the format directly
280
365
 
281
- All of this lives in `@kerusiweb/core` and is pure — no Angular import — so it
366
+ All of this lives in `@kerusiweb/core` and is pure (no Angular import), so it
282
367
  can run in a test, a build step or on a server.
283
368
 
284
369
  ```ts
@@ -327,7 +412,7 @@ Deltas that are stale, duplicate or scoped to another session are discarded.
327
412
  > **Gap detection needs a sequence.** §5.2 requires `updatedAt` to be strictly
328
413
  > increasing but not contiguous, so it cannot by itself distinguish "a delta was
329
414
  > lost" from "nothing happened for a while". The store detects gaps when the
330
- > transport supplies a monotonic sequence — `metadata.seq` by default, or your
415
+ > transport supplies a monotonic sequence: `metadata.seq` by default, or your
331
416
  > own `sequenceOf` reader. A gapped delta is still applied, so the map degrades
332
417
  > rather than freezes while you re-fetch.
333
418