@kerusiweb/react 2.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/LICENSE +22 -0
- package/README.md +306 -0
- package/dist/kerusi-legend.d.ts +29 -0
- package/dist/kerusi-legend.d.ts.map +1 -0
- package/dist/kerusi-legend.js +80 -0
- package/dist/kerusi-legend.js.map +1 -0
- package/dist/kerusi-seatmap.d.ts +20 -0
- package/dist/kerusi-seatmap.d.ts.map +1 -0
- package/dist/kerusi-seatmap.js +269 -0
- package/dist/kerusi-seatmap.js.map +1 -0
- package/dist/kerusi-section.d.ts +45 -0
- package/dist/kerusi-section.d.ts.map +1 -0
- package/dist/kerusi-section.js +102 -0
- package/dist/kerusi-section.js.map +1 -0
- package/dist/public-api.d.ts +22 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +18 -0
- package/dist/public-api.js.map +1 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/use-kerusi-state.d.ts +49 -0
- package/dist/use-kerusi-state.d.ts.map +1 -0
- package/dist/use-kerusi-state.js +82 -0
- package/dist/use-kerusi-state.js.map +1 -0
- package/dist/use-roving-focus.d.ts +27 -0
- package/dist/use-roving-focus.d.ts.map +1 -0
- package/dist/use-roving-focus.js +66 -0
- package/dist/use-roving-focus.js.map +1 -0
- package/package.json +52 -0
- package/styles.css +230 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-2026 Arsyad "Shad Ahm" Ahmad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# @kerusiweb/react
|
|
2
|
+
|
|
3
|
+
A React seat map that renders [Kerusi](https://github.com/ShadAhm/kerusi)
|
|
4
|
+
documents as interactive, accessible inline SVG.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @kerusiweb/core @kerusiweb/react
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requires React 18.3+ or 19. Function components and hooks; no context, no
|
|
11
|
+
provider, no global state.
|
|
12
|
+
|
|
13
|
+
This package is the React binding only — the components and the `useKerusiState`
|
|
14
|
+
hook. The document types, the conformance validator, the render model and its
|
|
15
|
+
geometry live in its peer
|
|
16
|
+
[`@kerusiweb/core`](https://www.npmjs.com/package/@kerusiweb/core), which is
|
|
17
|
+
framework-free; import them from there.
|
|
18
|
+
|
|
19
|
+
## Import the stylesheet
|
|
20
|
+
|
|
21
|
+
Once, anywhere in your app:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import '@kerusiweb/react/styles.css';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It carries layout, opacity, cursor, paint order and the focus ring — nothing
|
|
28
|
+
else. Every colour is bound inline by the components, so the theming tiers below
|
|
29
|
+
keep working.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## `<KerusiSeatmap>`
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { useState } from 'react';
|
|
37
|
+
import { KerusiSeatmap } from '@kerusiweb/react';
|
|
38
|
+
import type { KerusiMap, KerusiState } from '@kerusiweb/core';
|
|
39
|
+
import '@kerusiweb/react/styles.css';
|
|
40
|
+
|
|
41
|
+
export function Booking({ map, state }: { map: KerusiMap; state: KerusiState }) {
|
|
42
|
+
const [picked, setPicked] = useState<readonly string[]>([]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<KerusiSeatmap
|
|
46
|
+
map={map}
|
|
47
|
+
state={state}
|
|
48
|
+
selection={picked}
|
|
49
|
+
onSelectionChange={setPicked}
|
|
50
|
+
showLegend
|
|
51
|
+
onSeatDisallowed={(e) => explain(e.reason)}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`selection` is a plain list of seat ids. Nothing is mutated, so it works with
|
|
58
|
+
`useState`, a reducer, or any store you already have.
|
|
59
|
+
|
|
60
|
+
### Props
|
|
61
|
+
|
|
62
|
+
**Documents**
|
|
63
|
+
|
|
64
|
+
| Prop | Type | Default | |
|
|
65
|
+
| --------- | --------------- | ---------- | --------------------------------------------------------------------------- |
|
|
66
|
+
| `map` | `KerusiMap` | _required_ | The static venue layout. |
|
|
67
|
+
| `state` | `KerusiState` | — | Live availability. Merged by `Seat.id`; an absent seat is available (§5.1). |
|
|
68
|
+
| `session` | `KerusiSession` | — | The optional map↔event join (§5.3). Validated against the map and state. |
|
|
69
|
+
|
|
70
|
+
**Selection**
|
|
71
|
+
|
|
72
|
+
| Prop | Type | Default | |
|
|
73
|
+
| -------------------- | ------------------------------- | --------------- | ----------------------------------------------------------------------- |
|
|
74
|
+
| `selection` | `readonly string[]` | — | Selected seat ids. Supplying this makes the component controlled. |
|
|
75
|
+
| `defaultSelection` | `readonly string[]` | `[]` | The initial selection when uncontrolled. Ignored if `selection` is set. |
|
|
76
|
+
| `onSelectionChange` | `(selection) => void` | — | Fires on every change, controlled or not. |
|
|
77
|
+
| `selectableStatuses` | `SeatRenderStatus[]` | `['available']` | Which statuses a seat may be picked in. |
|
|
78
|
+
| `companionMode` | `'auto' \| 'independent'` | `'auto'` | `auto` selects a seat's whole companion closure together (§4.6). |
|
|
79
|
+
| `maxSelection` | `number` | — | Cap on selected seats. Counts a companion closure as its full size. |
|
|
80
|
+
| `seatSelectable` | `(seat: RenderSeat) => boolean` | — | A final say, applied after the status test. **Memoize it** — see below. |
|
|
81
|
+
| `interactive` | `boolean` | `true` | `false` renders read-only. |
|
|
82
|
+
|
|
83
|
+
Controlled and uncontrolled work the way they do for `<input>`: pass
|
|
84
|
+
`selection` + `onSelectionChange` to own it yourself, or pass neither and let
|
|
85
|
+
the component keep it. Passing `selection` without `onSelectionChange` renders a
|
|
86
|
+
selection the user cannot change — occasionally what you want, usually a
|
|
87
|
+
mistake.
|
|
88
|
+
|
|
89
|
+
**Appearance**
|
|
90
|
+
|
|
91
|
+
| Prop | Type | Default | |
|
|
92
|
+
| --------------------------------- | --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| `colors` | `KerusiSeatmapColors` | theme defaults | Per-status fills, element tones, focus ring, backdrop. See [Theming](#theming). |
|
|
94
|
+
| `typeColors` | `boolean` | `true` | Let an available seat take its `SeatType.color` (§4.7). |
|
|
95
|
+
| `seatSize` | `number` | `28` | Grid cell edge, in viewBox units. |
|
|
96
|
+
| `seatGap` | `number` | `6` | Gap between grid cells. |
|
|
97
|
+
| `freeformBasis` | `number` | `1000` | Freeform viewBox width; height follows the aspect ratio. |
|
|
98
|
+
| `unitScale` | `number` | `1` | CSS pixels per viewBox unit. Caps each section at its natural size so a narrow section and a wide one draw seats the same size. |
|
|
99
|
+
| `showSectionLabels` | `boolean` | `true` | |
|
|
100
|
+
| `showLegend` / `showLegendPrices` | `boolean` | `false` / `true` | |
|
|
101
|
+
| `className` / `style` | — | — | Applied to the root element, alongside `kerusi-seatmap`. |
|
|
102
|
+
|
|
103
|
+
**Sections**
|
|
104
|
+
|
|
105
|
+
| Prop | Type | |
|
|
106
|
+
| ------------------ | -------------------------------------- | ------------------------------------------------------------------- |
|
|
107
|
+
| `sectionIds` | `readonly string[]` | Render only these, in this order. Default: all, by `Section.index`. |
|
|
108
|
+
| `sectionOverrides` | `Record<string, SectionRenderOptions>` | Per-section `hidden`, `aspectRatio`, `seatSize`, `label`. |
|
|
109
|
+
|
|
110
|
+
**Localization, validation, lifecycle**
|
|
111
|
+
|
|
112
|
+
| Prop | Type | Default | |
|
|
113
|
+
| ------------------ | ------------------------------- | ---------------------------- | ---------------------------------------------------------------------- |
|
|
114
|
+
| `locale` | `string` | `KerusiMap.locale` ?? `'en'` | BCP-47. Resolves `Section.label` / `SeatType.label` locale maps. |
|
|
115
|
+
| `rtl` | `boolean \| 'auto'` | `'auto'` | Mirrors layout and arrow direction. `auto` derives it from the locale. |
|
|
116
|
+
| `ariaStrings` | `SeatAriaStrings` | English | Every announced phrase, for translation. |
|
|
117
|
+
| `validate` | `'collect' \| 'throw' \| 'off'` | `'collect'` | `collect` reports through `onValidationIssues` and renders anyway. |
|
|
118
|
+
| `expireHolds` | `boolean` | `false` | Revert a lapsed `holdExpires` to available on a ticker. |
|
|
119
|
+
| `expiryIntervalMs` | `number` | `1000` | |
|
|
120
|
+
|
|
121
|
+
`validate="throw"` throws a `KerusiValidationError` **during render**, so an
|
|
122
|
+
error boundary catches it. `collect` never throws.
|
|
123
|
+
|
|
124
|
+
### Callbacks
|
|
125
|
+
|
|
126
|
+
| Prop | Payload | |
|
|
127
|
+
| --------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
128
|
+
| `onSelectionChange` | `readonly string[]` | The selection after the change. |
|
|
129
|
+
| `onSeatSelect` / `onSeatDeselect` | `SeatInteraction` | The `RenderSeat`, the resulting selection, and every seat that changed (companions included). |
|
|
130
|
+
| `onSeatDisallowed` | `SeatDisallowed` | `reason` is `booked`, `held`, `blocked`, `not-selectable`, `max-selection` or `companion-unavailable`. |
|
|
131
|
+
| `onSeatFocus` | `RenderSeat` | |
|
|
132
|
+
| `onValidationIssues` | `readonly KerusiViolation[]` | Called whenever the documents change. |
|
|
133
|
+
|
|
134
|
+
### The `ref`
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
const map = useRef<KerusiSeatmapHandle>(null);
|
|
138
|
+
// map.current.summary -> { seats, total, unpriced }
|
|
139
|
+
// map.current.focusSeat('A12')
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Memoizing props
|
|
143
|
+
|
|
144
|
+
The render model is rebuilt whenever `map`, `state` or the interaction props
|
|
145
|
+
change identity. Object and function props declared inline get a new identity
|
|
146
|
+
every render, so hoist or memoize the ones that are not primitives:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
const seatSelectable = useCallback((seat: RenderSeat) => seat.price != null, []);
|
|
150
|
+
const colors = useMemo(() => ({ selectedBg: '#8b6ad6' }), []);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`map` and `state` are usually stable already if they come from state or a query
|
|
154
|
+
cache. `selectableStatuses` and `sectionOverrides` have stable defaults, so you
|
|
155
|
+
only need to think about them if you pass your own.
|
|
156
|
+
|
|
157
|
+
### The seat glyph
|
|
158
|
+
|
|
159
|
+
**Colour means seat type; shape means status.** A seat's fill is always its
|
|
160
|
+
`SeatType.color`, or the theme's `availableBg` when the type has none — it does
|
|
161
|
+
not change when the seat is held or booked. `selected` is the one deliberate
|
|
162
|
+
exception: it owns the seat's colour outright, so your own picks are never
|
|
163
|
+
ambiguous, and a purple fill on its own would in any case fail WCAG 1.4.1 (Use
|
|
164
|
+
of Colour) for anyone who cannot distinguish it — hence the shape cues. Held
|
|
165
|
+
seats take a wash and a hollow occupant figure; booked ones a wash and a solid
|
|
166
|
+
figure; selected ones a bright core inside a frame.
|
|
167
|
+
|
|
168
|
+
### Theming
|
|
169
|
+
|
|
170
|
+
Colors resolve through three tiers, highest first:
|
|
171
|
+
|
|
172
|
+
1. A `--kerusi-*` custom property in **your** stylesheet.
|
|
173
|
+
2. The **`colors` prop** — a partial `KerusiSeatmapColors`, merged over the defaults.
|
|
174
|
+
3. The library default.
|
|
175
|
+
|
|
176
|
+
The library never writes a `--kerusi-*` property onto its own root, which is
|
|
177
|
+
what keeps that order true: every fill is emitted as
|
|
178
|
+
`var(--kerusi-selected-bg, <the resolved prop value>)`. Use whichever tier fits
|
|
179
|
+
— the prop for values known at build time, CSS for anything that has to respond
|
|
180
|
+
to a media query or a theme class.
|
|
181
|
+
|
|
182
|
+
```css
|
|
183
|
+
.kerusi-seatmap {
|
|
184
|
+
--kerusi-selected-bg: #8b6ad6;
|
|
185
|
+
--kerusi-available-bg: #2f6b45;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@media (prefers-color-scheme: dark) {
|
|
189
|
+
.kerusi-seatmap {
|
|
190
|
+
--kerusi-element-bg: #232a36;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Every key of `KerusiSeatmapColors` has a property, kebab-cased:
|
|
196
|
+
|
|
197
|
+
| Prop key | Custom property |
|
|
198
|
+
| ------------------------------------- | ----------------------------------------------------------- |
|
|
199
|
+
| `availableBg` / `availableFg` | `--kerusi-available-bg` / `--kerusi-available-fg` |
|
|
200
|
+
| `selectedBg` / `selectedFg` | `--kerusi-selected-bg` / `--kerusi-selected-fg` |
|
|
201
|
+
| `heldBg` / `heldFg` | `--kerusi-held-bg` / `--kerusi-held-fg` |
|
|
202
|
+
| `bookedBg` / `bookedFg` | `--kerusi-booked-bg` / `--kerusi-booked-fg` |
|
|
203
|
+
| `blockedBg` / `blockedFg` | `--kerusi-blocked-bg` / `--kerusi-blocked-fg` |
|
|
204
|
+
| `elementBg` / `elementFg` | `--kerusi-element-bg` / `--kerusi-element-fg` |
|
|
205
|
+
| `elementAccentBg` / `elementAccentFg` | `--kerusi-element-accent-bg` / `--kerusi-element-accent-fg` |
|
|
206
|
+
| `elementMutedBg` / `elementMutedFg` | `--kerusi-element-muted-bg` / `--kerusi-element-muted-fg` |
|
|
207
|
+
| `focusRing` | `--kerusi-focus-ring` |
|
|
208
|
+
| `backdrop` | `--kerusi-backdrop` |
|
|
209
|
+
|
|
210
|
+
### Keyboard
|
|
211
|
+
|
|
212
|
+
Tab moves between sections; each section keeps its own tab stop. Within a
|
|
213
|
+
section, arrow keys follow the §4.3.1 `col` order — so an aisle, which is a
|
|
214
|
+
column no seat occupies, is stepped across rather than into.
|
|
215
|
+
|
|
216
|
+
| Key | |
|
|
217
|
+
| ------------------------ | ---------------------------------- |
|
|
218
|
+
| `←` `→` | Previous / next seat in the row |
|
|
219
|
+
| `↑` `↓` | Nearest column in the adjacent row |
|
|
220
|
+
| `Home` / `End` | First / last seat of the row |
|
|
221
|
+
| `Ctrl+Home` / `Ctrl+End` | First / last seat of the section |
|
|
222
|
+
| `PageUp` / `PageDown` | First / last seat of the section |
|
|
223
|
+
| `Enter` / `Space` | Toggle |
|
|
224
|
+
| `Escape` | Clear the selection |
|
|
225
|
+
|
|
226
|
+
Each seat is a `role="button"` announcing its position, type, price, status,
|
|
227
|
+
every `Seat.accessibility` property and its attributes. A polite live region
|
|
228
|
+
reports the running selection and any disallowed reason.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## `<KerusiLegend>`
|
|
233
|
+
|
|
234
|
+
Rendered inline by `showLegend`, or placed anywhere yourself:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
<KerusiLegend legend={model.legend} locale="ms" showPrices />
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
It resolves swatches through the same path as the seat fills, so the two cannot
|
|
241
|
+
drift apart. Every availability swatch — not just **Selected** — draws the seat
|
|
242
|
+
glyph itself rather than a flat colour, because shape is the cue the map uses
|
|
243
|
+
for status.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## `useKerusiState`
|
|
248
|
+
|
|
249
|
+
Live availability, fed by deltas from whatever transport you use. The ordering,
|
|
250
|
+
gap detection and hold expiry all come from `@kerusiweb/core`; the hook only
|
|
251
|
+
holds them in React state.
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
const availability = useKerusiState(initialState, { expiryIntervalMs: 1000 });
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
const socket = new WebSocket(url);
|
|
258
|
+
socket.onmessage = (e) => {
|
|
259
|
+
if (availability.apply(JSON.parse(e.data)).outcome === 'gap') {
|
|
260
|
+
refetch().then(availability.reset);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
return () => socket.close();
|
|
264
|
+
}, [availability.apply, availability.reset]);
|
|
265
|
+
|
|
266
|
+
<KerusiSeatmap map={map} state={availability.state} />;
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`apply`, `reset` and `tick` keep a stable identity across renders, so an effect
|
|
270
|
+
can subscribe once. It also exposes `needsRefetch` and `heldSeats` — every
|
|
271
|
+
currently-held seat with the time left on its hold, soonest first.
|
|
272
|
+
|
|
273
|
+
Deltas that are stale, duplicate or scoped to another session are discarded.
|
|
274
|
+
|
|
275
|
+
> **Gap detection needs a sequence.** §5.2 requires `updatedAt` to be strictly
|
|
276
|
+
> increasing but not contiguous, so it cannot by itself distinguish "a delta was
|
|
277
|
+
> lost" from "nothing happened for a while". The hook detects gaps when the
|
|
278
|
+
> transport supplies a monotonic sequence — `metadata.seq` by default, or your
|
|
279
|
+
> own `sequenceOf` reader. A gapped delta is still applied, so the map degrades
|
|
280
|
+
> rather than freezes while you re-fetch.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Working with the format directly
|
|
285
|
+
|
|
286
|
+
All of this lives in `@kerusiweb/core` and is pure — no React import — so it can
|
|
287
|
+
run in a test, a build step, a server component or a route loader.
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import {
|
|
291
|
+
buildRenderModel, // KerusiMap + KerusiState -> the resolved render model
|
|
292
|
+
checkKerusiMap, // every violation, no throw
|
|
293
|
+
validateKerusiMap, // throws on the first error
|
|
294
|
+
validateDocumentSet, // map / session / state joins
|
|
295
|
+
resolveSeatPrice, // the §4.9 precedence order
|
|
296
|
+
resolveLocalizedText, // string | Record<string, string> -> string
|
|
297
|
+
formatMoney, // minor units -> "RM 45.00", "¥1,200", "KD 12.500"
|
|
298
|
+
summarizeSelection, // seats, total, unpriced count
|
|
299
|
+
computeSectionLayout, // placed geometry for one section
|
|
300
|
+
buildNavigationGraph, // per-seat keyboard neighbours
|
|
301
|
+
} from '@kerusiweb/core';
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
MIT © Arsyad "Shad Ahm" Ahmad
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { KerusiSeatmapColors, RenderLegendEntry } from '@kerusiweb/core';
|
|
2
|
+
export interface KerusiLegendProps {
|
|
3
|
+
/** From `RenderMap.legend`. */
|
|
4
|
+
legend: readonly RenderLegendEntry[];
|
|
5
|
+
locale?: string;
|
|
6
|
+
colors?: Required<KerusiSeatmapColors>;
|
|
7
|
+
/** Whether an available seat takes its type color — mirrors the seatmap prop. */
|
|
8
|
+
typeColors?: boolean;
|
|
9
|
+
showPrices?: boolean;
|
|
10
|
+
/** Hide types no seat in the rendered map actually uses. */
|
|
11
|
+
hideUnusedTypes?: boolean;
|
|
12
|
+
headingSeatTypes?: string;
|
|
13
|
+
headingAvailability?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A key for a seat map: its seat types on one side, its availability states on
|
|
17
|
+
* the other.
|
|
18
|
+
*
|
|
19
|
+
* This ships in the library rather than being left to each application because
|
|
20
|
+
* the swatch colors must come from the *same* resolution path as the seat
|
|
21
|
+
* fills — `SeatType.color` when the document supplies one, the theme's
|
|
22
|
+
* available color when it does not. Re-deriving that in application code is how
|
|
23
|
+
* a legend ends up disagreeing with the map it describes.
|
|
24
|
+
*
|
|
25
|
+
* It is opt-in: `<KerusiSeatmap showLegend />` renders it inline, or use
|
|
26
|
+
* `<KerusiLegend>` directly to place it anywhere.
|
|
27
|
+
*/
|
|
28
|
+
export declare function KerusiLegend({ legend, locale, colors, typeColors, showPrices, hideUnusedTypes, headingSeatTypes, headingAvailability, }: KerusiLegendProps): import("react").JSX.Element;
|
|
29
|
+
//# sourceMappingURL=kerusi-legend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kerusi-legend.d.ts","sourceRoot":"","sources":["../src/kerusi-legend.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAuB,MAAM,iBAAiB,CAAC;AAcnG,MAAM,WAAW,iBAAiB;IAChC,+BAA+B;IAC/B,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAC;IACvC,iFAAiF;IACjF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4DAA4D;IAC5D,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAuBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,MAAa,EACb,MAA8B,EAC9B,UAAiB,EACjB,UAAiB,EACjB,eAAsB,EACtB,gBAA+B,EAC/B,mBAAoC,GACrC,EAAE,iBAAiB,+BAsFnB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cssVar, DEFAULT_KERUSI_COLORS, formatMoney, occupantFillFor, occupantStrokeFor, readableOn, seatBodyPath, seatCoreFill, seatOccupantPath, seatOccupantStroke, seatSelectedFrame, seatWashFill, } from '@kerusiweb/core';
|
|
3
|
+
/** The swatch's viewBox edge; the seat glyph is drawn to fill it. */
|
|
4
|
+
const GLYPH_SIZE = 16;
|
|
5
|
+
const GLYPH_BODY = seatBodyPath(0, 0, GLYPH_SIZE, GLYPH_SIZE);
|
|
6
|
+
/** The selected swatch's frame width, and the core it leaves inside itself. */
|
|
7
|
+
const GLYPH_FRAME = seatSelectedFrame(GLYPH_SIZE, GLYPH_SIZE);
|
|
8
|
+
const GLYPH_CORE_SIZE = GLYPH_SIZE - GLYPH_FRAME * 2;
|
|
9
|
+
const GLYPH_CORE = seatBodyPath(0, 0, GLYPH_SIZE, GLYPH_SIZE, GLYPH_FRAME);
|
|
10
|
+
const GLYPH_OCCUPANT = seatOccupantPath(0, 0, GLYPH_SIZE, GLYPH_SIZE);
|
|
11
|
+
const GLYPH_OCCUPANT_STROKE = seatOccupantStroke(GLYPH_SIZE, GLYPH_SIZE);
|
|
12
|
+
/** The selected swatch's figure sits on the core, so it is drawn to the core. */
|
|
13
|
+
const GLYPH_CORE_OCCUPANT = seatOccupantPath(GLYPH_FRAME, GLYPH_FRAME, GLYPH_CORE_SIZE, GLYPH_CORE_SIZE);
|
|
14
|
+
const GLYPH_CORE_OCCUPANT_STROKE = seatOccupantStroke(GLYPH_CORE_SIZE, GLYPH_CORE_SIZE);
|
|
15
|
+
/**
|
|
16
|
+
* A key for a seat map: its seat types on one side, its availability states on
|
|
17
|
+
* the other.
|
|
18
|
+
*
|
|
19
|
+
* This ships in the library rather than being left to each application because
|
|
20
|
+
* the swatch colors must come from the *same* resolution path as the seat
|
|
21
|
+
* fills — `SeatType.color` when the document supplies one, the theme's
|
|
22
|
+
* available color when it does not. Re-deriving that in application code is how
|
|
23
|
+
* a legend ends up disagreeing with the map it describes.
|
|
24
|
+
*
|
|
25
|
+
* It is opt-in: `<KerusiSeatmap showLegend />` renders it inline, or use
|
|
26
|
+
* `<KerusiLegend>` directly to place it anywhere.
|
|
27
|
+
*/
|
|
28
|
+
export function KerusiLegend({ legend, locale = 'en', colors = DEFAULT_KERUSI_COLORS, typeColors = true, showPrices = true, hideUnusedTypes = true, headingSeatTypes = 'Seat types', headingAvailability = 'Availability', }) {
|
|
29
|
+
const types = legend.filter((entry) => !hideUnusedTypes || entry.seatCount > 0);
|
|
30
|
+
const statuses = statusEntries(colors);
|
|
31
|
+
return (_jsxs("div", { className: "kerusi-legend", children: [types.length > 0 && (_jsxs("div", { className: "kerusi-legend__block", children: [_jsx("h4", { className: "kerusi-legend__heading", children: headingSeatTypes }), _jsx("ul", { className: "kerusi-legend__list", children: types.map((entry) => {
|
|
32
|
+
const typeColor = (typeColors && entry.color) || null;
|
|
33
|
+
const price = entry.price ? formatMoney(entry.price, locale) : '';
|
|
34
|
+
return (_jsxs("li", { className: "kerusi-legend__item", children: [_jsx("span", { className: "kerusi-legend__swatch", "aria-hidden": "true", style: {
|
|
35
|
+
background: typeColor ?? cssVar('availableBg', colors.availableBg),
|
|
36
|
+
// Mirrors `seatTextFill`: only a document-supplied color
|
|
37
|
+
// needs guessing at.
|
|
38
|
+
color: typeColor
|
|
39
|
+
? readableOn(typeColor)
|
|
40
|
+
: cssVar('availableFg', colors.availableFg),
|
|
41
|
+
} }), _jsx("span", { className: "kerusi-legend__label", children: entry.label }), showPrices && price && _jsx("span", { className: "kerusi-legend__price", children: price })] }, entry.id));
|
|
42
|
+
}) })] })), _jsxs("div", { className: "kerusi-legend__block", children: [_jsx("h4", { className: "kerusi-legend__heading", children: headingAvailability }), _jsx("ul", { className: "kerusi-legend__list", children: statuses.map((status) => (_jsxs("li", { className: "kerusi-legend__item", children: [_jsxs("svg", { className: "kerusi-legend__swatch kerusi-legend__swatch--glyph", "aria-hidden": "true", focusable: "false", viewBox: `0 0 ${GLYPH_SIZE} ${GLYPH_SIZE}`, children: [_jsx("path", { d: GLYPH_BODY, style: { fill: status.fill } }), hasWash(status.kind) && (_jsx("path", { d: GLYPH_BODY, style: { fill: seatWashFill(status.kind, colors) }, opacity: "0.5" })), status.kind === 'selected' && (_jsx("path", { d: GLYPH_CORE, style: { fill: seatCoreFill(colors) } })), hasOccupant(status.kind) && (_jsx("path", { d: status.kind === 'selected' ? GLYPH_CORE_OCCUPANT : GLYPH_OCCUPANT, className: `kerusi-legend__occupant kerusi-legend__occupant--${status.kind}`, strokeWidth: status.kind === 'selected'
|
|
43
|
+
? GLYPH_CORE_OCCUPANT_STROKE
|
|
44
|
+
: GLYPH_OCCUPANT_STROKE, style: {
|
|
45
|
+
fill: occupantFillFor(status.kind, colors),
|
|
46
|
+
stroke: occupantStrokeFor(status.kind, colors),
|
|
47
|
+
} }))] }), _jsx("span", { className: "kerusi-legend__label", children: status.label })] }, status.key))) })] })] }));
|
|
48
|
+
}
|
|
49
|
+
// Colour means seat type; shape means status (see `seatFill`'s doc comment).
|
|
50
|
+
// Held and booked swatches share the plain "available" body colour — the map's
|
|
51
|
+
// colour there is always whatever the seat's own type supplies, which the
|
|
52
|
+
// legend has no single value for — and are told apart by their wash and
|
|
53
|
+
// occupant figure instead, same as on the map.
|
|
54
|
+
function statusEntries(colors) {
|
|
55
|
+
const available = cssVar('availableBg', colors.availableBg);
|
|
56
|
+
return [
|
|
57
|
+
{ key: 'available', label: 'Available', kind: 'plain', fill: available },
|
|
58
|
+
{
|
|
59
|
+
key: 'selected',
|
|
60
|
+
label: 'Selected',
|
|
61
|
+
kind: 'selected',
|
|
62
|
+
fill: cssVar('selectedBg', colors.selectedBg),
|
|
63
|
+
},
|
|
64
|
+
{ key: 'held', label: 'On hold', kind: 'held', fill: available },
|
|
65
|
+
{ key: 'booked', label: 'Booked', kind: 'booked', fill: available },
|
|
66
|
+
{
|
|
67
|
+
key: 'blocked',
|
|
68
|
+
label: 'Blocked',
|
|
69
|
+
kind: 'blocked',
|
|
70
|
+
fill: cssVar('blockedBg', colors.blockedBg),
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
function hasWash(kind) {
|
|
75
|
+
return kind === 'held' || kind === 'booked';
|
|
76
|
+
}
|
|
77
|
+
function hasOccupant(kind) {
|
|
78
|
+
return kind === 'selected' || kind === 'held' || kind === 'booked';
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=kerusi-legend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kerusi-legend.js","sourceRoot":"","sources":["../src/kerusi-legend.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,GACb,MAAM,iBAAiB,CAAC;AA6BzB,qEAAqE;AACrE,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,UAAU,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AAE9D,+EAA+E;AAC/E,MAAM,WAAW,GAAG,iBAAiB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;AAC9D,MAAM,eAAe,GAAG,UAAU,GAAG,WAAW,GAAG,CAAC,CAAC;AACrD,MAAM,UAAU,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;AAE3E,MAAM,cAAc,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AACtE,MAAM,qBAAqB,GAAG,kBAAkB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;AAEzE,iFAAiF;AACjF,MAAM,mBAAmB,GAAG,gBAAgB,CAC1C,WAAW,EACX,WAAW,EACX,eAAe,EACf,eAAe,CAChB,CAAC;AACF,MAAM,0BAA0B,GAAG,kBAAkB,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;AAExF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,MAAM,EACN,MAAM,GAAG,IAAI,EACb,MAAM,GAAG,qBAAqB,EAC9B,UAAU,GAAG,IAAI,EACjB,UAAU,GAAG,IAAI,EACjB,eAAe,GAAG,IAAI,EACtB,gBAAgB,GAAG,YAAY,EAC/B,mBAAmB,GAAG,cAAc,GAClB;IAClB,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,eAAe,IAAI,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IAChF,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAEvC,OAAO,CACL,eAAK,SAAS,EAAC,eAAe,aAC3B,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CACnB,eAAK,SAAS,EAAC,sBAAsB,aACnC,aAAI,SAAS,EAAC,wBAAwB,YAAE,gBAAgB,GAAM,EAC9D,aAAI,SAAS,EAAC,qBAAqB,YAChC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;4BACnB,MAAM,SAAS,GAAG,CAAC,UAAU,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;4BACtD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;4BAClE,OAAO,CACL,cAAmB,SAAS,EAAC,qBAAqB,aAChD,eACE,SAAS,EAAC,uBAAuB,iBACrB,MAAM,EAClB,KAAK,EAAE;4CACL,UAAU,EAAE,SAAS,IAAI,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,WAAW,CAAC;4CAClE,yDAAyD;4CACzD,qBAAqB;4CACrB,KAAK,EAAE,SAAS;gDACd,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC;gDACvB,CAAC,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,WAAW,CAAC;yCAC9C,GACD,EACF,eAAM,SAAS,EAAC,sBAAsB,YAAE,KAAK,CAAC,KAAK,GAAQ,EAC1D,UAAU,IAAI,KAAK,IAAI,eAAM,SAAS,EAAC,sBAAsB,YAAE,KAAK,GAAQ,KAdtE,KAAK,CAAC,EAAE,CAeZ,CACN,CAAC;wBACJ,CAAC,CAAC,GACC,IACD,CACP,EAED,eAAK,SAAS,EAAC,sBAAsB,aACnC,aAAI,SAAS,EAAC,wBAAwB,YAAE,mBAAmB,GAAM,EACjE,aAAI,SAAS,EAAC,qBAAqB,YAChC,QAAQ,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CACxB,cAAqB,SAAS,EAAC,qBAAqB,aAIlD,eACE,SAAS,EAAC,oDAAoD,iBAClD,MAAM,EAClB,SAAS,EAAC,OAAO,EACjB,OAAO,EAAE,OAAO,UAAU,IAAI,UAAU,EAAE,aAE1C,eAAM,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAI,EACpD,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CACvB,eACE,CAAC,EAAE,UAAU,EACb,KAAK,EAAE,EAAE,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAClD,OAAO,EAAC,KAAK,GACb,CACH,EACA,MAAM,CAAC,IAAI,KAAK,UAAU,IAAI,CAG7B,eAAM,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,EAAE,GAAI,CAC/D,EACA,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAC3B,eACE,CAAC,EAAE,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,cAAc,EACpE,SAAS,EAAE,oDAAoD,MAAM,CAAC,IAAI,EAAE,EAC5E,WAAW,EACT,MAAM,CAAC,IAAI,KAAK,UAAU;gDACxB,CAAC,CAAC,0BAA0B;gDAC5B,CAAC,CAAC,qBAAqB,EAE3B,KAAK,EAAE;gDACL,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC;gDAC1C,MAAM,EAAE,iBAAiB,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC;6CAC/C,GACD,CACH,IACG,EACN,eAAM,SAAS,EAAC,sBAAsB,YAAE,MAAM,CAAC,KAAK,GAAQ,KAvCrD,MAAM,CAAC,GAAG,CAwCd,CACN,CAAC,GACC,IACD,IACF,CACP,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,+EAA+E;AAC/E,0EAA0E;AAC1E,wEAAwE;AACxE,+CAA+C;AAC/C,SAAS,aAAa,CAAC,MAAqC;IAC1D,MAAM,SAAS,GAAG,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5D,OAAO;QACL,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;QACxE;YACE,GAAG,EAAE,UAAU;YACf,KAAK,EAAE,UAAU;YACjB,IAAI,EAAE,UAAU;YAChB,IAAI,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,UAAU,CAAC;SAC9C;QACD,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE;QAChE,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE;QACnE;YACE,GAAG,EAAE,SAAS;YACd,KAAK,EAAE,SAAS;YAChB,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,SAAS,CAAC;SAC5C;KACF,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,IAAgB;IAC/B,OAAO,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,QAAQ,CAAC;AAC9C,CAAC;AAED,SAAS,WAAW,CAAC,IAAgB;IACnC,OAAO,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,QAAQ,CAAC;AACrE,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { KerusiSeatmapHandle, KerusiSeatmapProps } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Renders a Kerusi seat map.
|
|
4
|
+
*
|
|
5
|
+
* Takes a `KerusiMap` and its `KerusiState` directly — no adapter, no
|
|
6
|
+
* intermediate row model. Each `Section` becomes its own `<svg>` with its own
|
|
7
|
+
* layout mode and proportions, seats take their color from the map's legend,
|
|
8
|
+
* every seat is keyboard-reachable and announced with its type, price, status
|
|
9
|
+
* and §4.3.4 accessibility properties, and selection is an immutable list of
|
|
10
|
+
* seat ids.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <KerusiSeatmap map={map} state={state} selection={picked} onSelectionChange={setPicked} />
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Import the stylesheet once, anywhere in the app:
|
|
17
|
+
* `import '@kerusiweb/react/styles.css'`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const KerusiSeatmap: import("react").ForwardRefExoticComponent<KerusiSeatmapProps & import("react").RefAttributes<KerusiSeatmapHandle>>;
|
|
20
|
+
//# sourceMappingURL=kerusi-seatmap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kerusi-seatmap.d.ts","sourceRoot":"","sources":["../src/kerusi-seatmap.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EAGnB,MAAM,YAAY,CAAC;AAOpB;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,aAAa,oHAmazB,CAAC"}
|