@kerusiweb/react 2.3.0 → 3.1.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/styles.css CHANGED
@@ -1,365 +1,541 @@
1
- /*
2
- * @kerusiweb/react — the one stylesheet the package ships.
3
- *
4
- * import '@kerusiweb/react/styles.css';
5
- *
6
- * It owns layout, opacity, cursor, paint order and the focus ring, and nothing
7
- * else. Every colour is bound inline by the components instead, because a
8
- * `var(--kerusi-selected-bg, …)` written here would carry the *library* default
9
- * as its fallback and silently skip the `colors` prop tier — only the
10
- * TypeScript side can resolve that one. See `cssVar()` in @kerusiweb/core for
11
- * the three tiers and their precedence.
12
- */
13
-
14
- /* --- the map ------------------------------------------------------------- */
15
-
16
- .kerusi-seatmap {
17
- display: flex;
18
- flex-direction: column;
19
- gap: 1.25rem;
20
- }
21
-
22
- .kerusi-seatmap__section {
23
- display: flex;
24
- flex-direction: column;
25
- gap: 0.4rem;
26
- }
27
-
28
- .kerusi-section-label {
29
- margin: 0;
30
- font-size: 0.85rem;
31
- font-weight: 600;
32
- letter-spacing: 0.04em;
33
- text-transform: uppercase;
34
- opacity: 0.75;
35
- }
36
-
37
- /* Visually hidden, still announced. */
38
- .kerusi-sr-only {
39
- position: absolute;
40
- width: 1px;
41
- height: 1px;
42
- margin: -1px;
43
- padding: 0;
44
- overflow: hidden;
45
- clip: rect(0 0 0 0);
46
- clip-path: inset(50%);
47
- white-space: nowrap;
48
- border: 0;
49
- }
50
-
51
- /* --- a section ----------------------------------------------------------- */
52
-
53
- /* The viewBox carries the proportions; CSS only decides how wide it gets. */
54
- .kerusi-section-svg {
55
- display: block;
56
- width: 100%;
57
- height: auto;
58
- max-width: 100%;
59
- }
60
-
61
- .kerusi-seat {
62
- cursor: pointer;
63
- outline: none;
64
- }
65
-
66
- .kerusi-seat--unselectable {
67
- cursor: not-allowed;
68
- }
69
-
70
- .kerusi-seat__label,
71
- .kerusi-element__label {
72
- pointer-events: none;
73
- user-select: none;
74
- font-family: inherit;
75
- }
76
-
77
- /* A selected seat's number is stroked with the core colour and painted stroke
78
- first, so the halo sits behind the glyphs and knocks the occupant figure out
79
- from under them. Only the ordering lives here: the colour and width are bound
80
- inline, for the reason at the top of this file. */
81
- .kerusi-seat__label {
82
- paint-order: stroke;
83
- stroke-linejoin: round;
84
- }
85
-
86
- /* No `transition: fill` here, deliberately. Every fill is a
87
- `var(--kerusi-*, …)` substitution, and Chromium cannot transition a paint
88
- property to a variable-derived value — the transition never advances and the
89
- seat keeps its previous color for good, so a selected seat would never turn
90
- selected. That covers the core's fill and the label's halo stroke too.
91
- Correct color beats a 120ms fade. */
92
-
93
- /* Drawn over the seat's own type-colour fill, so a held or booked seat still
94
- recedes even though its hue no longer changes. Fill colour is bound inline;
95
- opacity is the only thing this rule owns. */
96
- .kerusi-seat__wash {
97
- pointer-events: none;
98
- opacity: 0.5;
99
- }
100
-
101
- /* The bright plate inside a selected seat's frame. Colour is bound inline;
102
- this rule only keeps it out of hit-testing, so a click still lands on the
103
- seat group. */
104
- .kerusi-seat__core {
105
- pointer-events: none;
106
- }
107
-
108
- /* Fill/stroke colour is bound inline per variant; these rules only set the
109
- weight each variant reads at. Solid = settled (yours, or sold); hollow = a
110
- hold still in progress. */
111
- .kerusi-seat__occupant {
112
- pointer-events: none;
113
- }
114
-
115
- /* Lighter than the other variants: this one is a dark mark on a bright plate,
116
- and a dark mark reads heavier than a light one at the same opacity. Kept as
117
- texture so it does not compete with the number sitting on top of it. */
118
- .kerusi-seat__occupant--selected {
119
- opacity: 0.2;
120
- }
121
-
122
- .kerusi-seat__occupant--booked {
123
- opacity: 0.55;
124
- }
125
-
126
- .kerusi-seat__occupant--held {
127
- opacity: 0.75;
128
- stroke-linecap: round;
129
- stroke-linejoin: round;
130
- }
131
-
132
- /* The number competes with the silhouette behind it. */
133
- .kerusi-seat--selected .kerusi-seat__label {
134
- font-weight: 600;
135
- }
136
-
137
- /* The focus ring is drawn on the seat box so it follows a rotated seat. It is
138
- a separate mark from the selected frame — it strokes the outermost path, so a
139
- focused selected seat shows both.
140
-
141
- Two levels of fallback, so this token keeps the same precedence as every
142
- other one: a `--kerusi-focus-ring` from the consumer's stylesheet wins;
143
- otherwise `--kerusi-focus-ring-input`, which the component writes on the
144
- `<svg>` from the `colors` prop; otherwise the inherited text color. */
145
- .kerusi-seat:focus-visible .kerusi-seat__box {
146
- stroke: var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));
147
- stroke-width: 2.5;
148
- paint-order: stroke;
149
- }
150
-
151
- /* With a custom seat template the library cannot assume a `.kerusi-seat__box`
152
- exists, so it draws this outline of the seat's body over the custom content
153
- instead (the stock seat never renders it). Same colour chain as the ring
154
- above; the stroke is centred on the outline because there is no fill of its
155
- own to sit behind. */
156
- .kerusi-seat__focus {
157
- fill: none;
158
- stroke: none;
159
- pointer-events: none;
160
- }
161
-
162
- .kerusi-seat:focus-visible .kerusi-seat__focus {
163
- stroke: var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));
164
- stroke-width: 2.5;
165
- }
166
-
167
- .kerusi-element__void {
168
- stroke-width: 1;
169
- stroke-dasharray: 4 3;
170
- opacity: 0.7;
171
- }
172
-
173
- /* --- the legend ---------------------------------------------------------- */
174
-
175
- .kerusi-legend {
176
- display: flex;
177
- flex-wrap: wrap;
178
- gap: 1.5rem;
179
- font-size: 0.85rem;
180
- }
181
-
182
- .kerusi-legend__block {
183
- display: flex;
184
- flex-direction: column;
185
- gap: 0.4rem;
186
- min-width: 0;
187
- }
188
-
189
- .kerusi-legend__heading {
190
- margin: 0;
191
- font-size: 0.72rem;
192
- font-weight: 600;
193
- letter-spacing: 0.06em;
194
- text-transform: uppercase;
195
- opacity: 0.65;
196
- }
197
-
198
- .kerusi-legend__list {
199
- display: flex;
200
- flex-wrap: wrap;
201
- gap: 0.35rem 0.9rem;
202
- margin: 0;
203
- padding: 0;
204
- list-style: none;
205
- }
206
-
207
- .kerusi-legend__item {
208
- display: inline-flex;
209
- align-items: center;
210
- gap: 0.4rem;
211
- }
212
-
213
- .kerusi-legend__swatch {
214
- display: inline-block;
215
- flex: none;
216
- width: 0.85rem;
217
- height: 0.85rem;
218
- border-radius: 3px;
219
- }
220
-
221
- /* The seat glyph carries its own corner radius in the viewBox. */
222
- .kerusi-legend__swatch--glyph {
223
- border-radius: 0;
224
- overflow: visible;
225
- }
226
-
227
- /* Mirrors `.kerusi-seat__occupant--*` on the map, so the key matches what it
228
- is keying. Fill/stroke colour is bound inline; these set the weight. */
229
- .kerusi-legend__occupant--selected {
230
- opacity: 0.2;
231
- }
232
-
233
- .kerusi-legend__occupant--booked {
234
- opacity: 0.55;
235
- }
236
-
237
- .kerusi-legend__occupant--held {
238
- opacity: 0.75;
239
- stroke-linecap: round;
240
- stroke-linejoin: round;
241
- }
242
-
243
- .kerusi-legend__price {
244
- opacity: 0.7;
245
- font-variant-numeric: tabular-nums;
246
- }
247
-
248
- /* --- zoom, pan and maximize -------------------------------------------------
249
- Everything below applies only when `zoomable` or `maximizable` is on; a plain
250
- map renders none of these elements. Colours are bound inline from the
251
- `--kerusi-*` tokens, so this file owns layout, motion and the focus ring.
252
- The same rules as the Angular component's stylesheet. */
253
-
254
- /* Same column the sections sit in without the wrapper, so turning a feature on
255
- does not move anything. */
256
- .kerusi-seatmap__stage {
257
- display: flex;
258
- flex-direction: column;
259
- gap: 1.25rem;
260
- }
261
-
262
- .kerusi-toolbar {
263
- display: flex;
264
- gap: 0.35rem;
265
- align-self: flex-end;
266
- /* Stays put under a maximized map's scrolling stage. */
267
- flex: none;
268
- }
269
-
270
- .kerusi-control {
271
- display: inline-flex;
272
- align-items: center;
273
- justify-content: center;
274
- box-sizing: border-box;
275
- /* 44px: a comfortable touch target on the phones this exists for. */
276
- width: 2.75rem;
277
- height: 2.75rem;
278
- padding: 0;
279
- border: 1px solid;
280
- border-radius: 0.4rem;
281
- font: inherit;
282
- cursor: pointer;
283
- }
284
-
285
- /* `aria-disabled`, not `disabled`: a disabled button drops out of the tab order,
286
- and pressing "zoom in" until the ceiling would throw focus away mid-press. */
287
- .kerusi-control[aria-disabled='true'] {
288
- opacity: 0.45;
289
- cursor: default;
290
- }
291
-
292
- .kerusi-control svg {
293
- width: 1.4rem;
294
- height: 1.4rem;
295
- fill: none;
296
- stroke: currentColor;
297
- stroke-width: 2;
298
- stroke-linecap: round;
299
- stroke-linejoin: round;
300
- pointer-events: none;
301
- }
302
-
303
- .kerusi-control:focus-visible,
304
- .kerusi-viewport:focus-visible {
305
- outline: 2px solid var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));
306
- outline-offset: 2px;
307
- }
308
-
309
- /* A window onto the map. `clip` rather than `hidden`: a hidden box can still be
310
- scrolled by the browser when a seat inside takes focus, which would fight the
311
- transform; a clipped one cannot be scrolled at all. */
312
- .kerusi-viewport {
313
- position: relative;
314
- overflow: hidden;
315
- overflow: clip;
316
- user-select: none;
317
- -webkit-user-select: none;
318
- }
319
-
320
- /* The same column the sections sit in outside a viewport. */
321
- .kerusi-viewport__content {
322
- display: flex;
323
- flex-direction: column;
324
- gap: 1.25rem;
325
- transform-origin: 0 0;
326
- }
327
-
328
- /* Only for a button, key or reveal. A finger or wheel moves the map directly, and
329
- easing behind it would feel like lag. */
330
- .kerusi-viewport__content--animated {
331
- transition: transform 160ms ease-out;
332
- }
333
-
334
- @media (prefers-reduced-motion: reduce) {
335
- .kerusi-viewport__content--animated {
336
- transition: none;
337
- }
338
- }
339
-
340
- /* Maximized: the map fills the screen. This is the whole feature on iPhone,
341
- which has no element full screen; elsewhere native full screen is requested
342
- on top. `Canvas` / `CanvasText` and the backdrop come from inline styles. */
343
- .kerusi-seatmap--maximized {
344
- position: fixed;
345
- inset: 0;
346
- z-index: var(--kerusi-maximized-z-index, 1000);
347
- box-sizing: border-box;
348
- padding: 0.75rem;
349
- gap: 0.5rem;
350
- overflow: hidden;
351
- overscroll-behavior: contain;
352
- }
353
-
354
- .kerusi-seatmap--maximized .kerusi-seatmap__stage {
355
- flex: 1 1 auto;
356
- min-height: 0;
357
- gap: 0.5rem;
358
- /* A map that is not zoomable has no viewport to pan, so it scrolls instead. */
359
- overflow: auto;
360
- }
361
-
362
- .kerusi-seatmap--maximized .kerusi-viewport {
363
- flex: 1 1 auto;
364
- min-height: 0;
365
- }
1
+ /*
2
+ * @kerusiweb/react — the one stylesheet the package ships.
3
+ *
4
+ * import '@kerusiweb/react/styles.css';
5
+ *
6
+ * It owns layout, opacity, cursor, paint order and the focus ring, and nothing
7
+ * else. Every colour is bound inline by the components instead, because a
8
+ * `var(--kerusi-selected-bg, …)` written here would carry the *library* default
9
+ * as its fallback and silently skip the `colors` prop tier — only the
10
+ * TypeScript side can resolve that one. See `cssVar()` in @kerusiweb/core for
11
+ * the three tiers and their precedence.
12
+ */
13
+
14
+ /* --- the map ------------------------------------------------------------- */
15
+
16
+ .kerusi-seatmap {
17
+ /* The containing block for a seat card, which is positioned in the map's root. */
18
+ position: relative;
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 1.25rem;
22
+ }
23
+
24
+ /* A section's label, drawn inside the map at the top-left of its bounds (D5).
25
+ Its size comes from the section's seat size, so it scales with the map. */
26
+ .kerusi-section-label {
27
+ font-family: inherit;
28
+ font-weight: 600;
29
+ letter-spacing: 0.04em;
30
+ text-transform: uppercase;
31
+ opacity: 0.75;
32
+ pointer-events: none;
33
+ user-select: none;
34
+ }
35
+
36
+ /* Visually hidden, still announced. */
37
+ .kerusi-sr-only {
38
+ position: absolute;
39
+ width: 1px;
40
+ height: 1px;
41
+ margin: -1px;
42
+ padding: 0;
43
+ overflow: hidden;
44
+ clip: rect(0 0 0 0);
45
+ clip-path: inset(50%);
46
+ white-space: nowrap;
47
+ border: 0;
48
+ }
49
+
50
+ /* --- the drawing: one svg, a group per section ---------------------------- */
51
+
52
+ /* The viewBox is the document's canvas and carries its proportions; CSS only
53
+ decides how wide it gets. */
54
+ .kerusi-seatmap__svg {
55
+ display: block;
56
+ width: 100%;
57
+ height: auto;
58
+ max-width: 100%;
59
+ }
60
+
61
+ .kerusi-seat {
62
+ cursor: pointer;
63
+ outline: none;
64
+ }
65
+
66
+ .kerusi-seat--unselectable {
67
+ cursor: not-allowed;
68
+ }
69
+
70
+ .kerusi-seat__label,
71
+ .kerusi-element__label {
72
+ pointer-events: none;
73
+ user-select: none;
74
+ font-family: inherit;
75
+ }
76
+
77
+ /* A selected seat's number is stroked with the core colour and painted stroke
78
+ first, so the halo sits behind the glyphs and knocks the occupant figure out
79
+ from under them. Only the ordering lives here: the colour and width are bound
80
+ inline, for the reason at the top of this file. */
81
+ .kerusi-seat__label {
82
+ paint-order: stroke;
83
+ stroke-linejoin: round;
84
+ }
85
+
86
+ /* No `transition: fill` here, deliberately. Every fill is a
87
+ `var(--kerusi-*, …)` substitution, and Chromium cannot transition a paint
88
+ property to a variable-derived value — the transition never advances and the
89
+ seat keeps its previous color for good, so a selected seat would never turn
90
+ selected. That covers the core's fill and the label's halo stroke too.
91
+ Correct color beats a 120ms fade. */
92
+
93
+ /* Drawn over the seat's own type-colour fill, so a held or booked seat still
94
+ recedes even though its hue no longer changes. Fill colour is bound inline;
95
+ opacity is the only thing this rule owns. */
96
+ .kerusi-seat__wash {
97
+ pointer-events: none;
98
+ opacity: 0.5;
99
+ }
100
+
101
+ /* The bright plate inside a selected seat's frame. Colour is bound inline;
102
+ this rule only keeps it out of hit-testing, so a click still lands on the
103
+ seat group. */
104
+ .kerusi-seat__core {
105
+ pointer-events: none;
106
+ }
107
+
108
+ /* Fill/stroke colour is bound inline per variant; these rules only set the
109
+ weight each variant reads at. Solid = settled (yours, or sold); hollow = a
110
+ hold still in progress. */
111
+ .kerusi-seat__occupant {
112
+ pointer-events: none;
113
+ }
114
+
115
+ /* Lighter than the other variants: this one is a dark mark on a bright plate,
116
+ and a dark mark reads heavier than a light one at the same opacity. Kept as
117
+ texture so it does not compete with the number sitting on top of it. */
118
+ .kerusi-seat__occupant--selected {
119
+ opacity: 0.2;
120
+ }
121
+
122
+ .kerusi-seat__occupant--booked {
123
+ opacity: 0.55;
124
+ }
125
+
126
+ .kerusi-seat__occupant--held {
127
+ opacity: 0.75;
128
+ stroke-linecap: round;
129
+ stroke-linejoin: round;
130
+ }
131
+
132
+ /* The number competes with the silhouette behind it. */
133
+ .kerusi-seat--selected .kerusi-seat__label {
134
+ font-weight: 600;
135
+ }
136
+
137
+ /* The focus ring is drawn on the seat box so it follows a rotated seat. It is
138
+ a separate mark from the selected frame — it strokes the outermost path, so a
139
+ focused selected seat shows both.
140
+
141
+ Two levels of fallback, so this token keeps the same precedence as every
142
+ other one: a `--kerusi-focus-ring` from the consumer's stylesheet wins;
143
+ otherwise `--kerusi-focus-ring-input`, which the component writes on the
144
+ `<svg>` from the `colors` prop; otherwise the inherited text color. */
145
+ .kerusi-seat:focus-visible .kerusi-seat__box {
146
+ stroke: var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));
147
+ stroke-width: 2.5;
148
+ paint-order: stroke;
149
+ }
150
+
151
+ /* With a custom seat template the library cannot assume a `.kerusi-seat__box`
152
+ exists, so it draws this outline of the seat's body over the custom content
153
+ instead (the stock seat never renders it). Same colour chain as the ring
154
+ above; the stroke is centred on the outline because there is no fill of its
155
+ own to sit behind. */
156
+ .kerusi-seat__focus {
157
+ fill: none;
158
+ stroke: none;
159
+ pointer-events: none;
160
+ }
161
+
162
+ .kerusi-seat:focus-visible .kerusi-seat__focus {
163
+ stroke: var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));
164
+ stroke-width: 2.5;
165
+ }
166
+
167
+ .kerusi-element__void {
168
+ stroke-width: 1;
169
+ stroke-dasharray: 4 3;
170
+ opacity: 0.7;
171
+ }
172
+
173
+ /* --- the legend ---------------------------------------------------------- */
174
+
175
+ .kerusi-legend {
176
+ display: flex;
177
+ flex-wrap: wrap;
178
+ gap: 1.5rem;
179
+ font-size: 0.85rem;
180
+ }
181
+
182
+ .kerusi-legend__block {
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 0.4rem;
186
+ min-width: 0;
187
+ }
188
+
189
+ .kerusi-legend__heading {
190
+ margin: 0;
191
+ font-size: 0.72rem;
192
+ font-weight: 600;
193
+ letter-spacing: 0.06em;
194
+ text-transform: uppercase;
195
+ opacity: 0.65;
196
+ }
197
+
198
+ .kerusi-legend__list {
199
+ display: flex;
200
+ flex-wrap: wrap;
201
+ gap: 0.35rem 0.9rem;
202
+ margin: 0;
203
+ padding: 0;
204
+ list-style: none;
205
+ }
206
+
207
+ .kerusi-legend__item {
208
+ display: inline-flex;
209
+ align-items: center;
210
+ gap: 0.4rem;
211
+ }
212
+
213
+ .kerusi-legend__swatch {
214
+ display: inline-block;
215
+ flex: none;
216
+ width: 0.85rem;
217
+ height: 0.85rem;
218
+ border-radius: 3px;
219
+ }
220
+
221
+ /* The seat glyph carries its own corner radius in the viewBox. */
222
+ .kerusi-legend__swatch--glyph {
223
+ border-radius: 0;
224
+ overflow: visible;
225
+ }
226
+
227
+ /* Mirrors `.kerusi-seat__occupant--*` on the map, so the key matches what it
228
+ is keying. Fill/stroke colour is bound inline; these set the weight. */
229
+ .kerusi-legend__occupant--selected {
230
+ opacity: 0.2;
231
+ }
232
+
233
+ .kerusi-legend__occupant--booked {
234
+ opacity: 0.55;
235
+ }
236
+
237
+ .kerusi-legend__occupant--held {
238
+ opacity: 0.75;
239
+ stroke-linecap: round;
240
+ stroke-linejoin: round;
241
+ }
242
+
243
+ .kerusi-legend__price {
244
+ opacity: 0.7;
245
+ font-variant-numeric: tabular-nums;
246
+ }
247
+
248
+ /* --- zoom, pan and maximize -------------------------------------------------
249
+ Everything below applies only when `zoomable` or `maximizable` is on; a plain
250
+ map renders none of these elements. Colours are bound inline from the
251
+ `--kerusi-*` tokens, so this file owns layout, motion and the focus ring.
252
+ The same rules as the Angular component's stylesheet. */
253
+
254
+ /* Same column the map sits in without the wrapper, so turning a feature on
255
+ does not move anything. */
256
+ .kerusi-seatmap__stage {
257
+ display: flex;
258
+ flex-direction: column;
259
+ gap: 1.25rem;
260
+ }
261
+
262
+ /* One grouped bar rather than four loose squares. Every measure and colour is a
263
+ `--kerusi-control-*` token so a host can restyle it from its own stylesheet;
264
+ the private `--kc-*` values below only resolve the tiers once. The `*-input`
265
+ colours are the `[colors]` palette, written inline by the component. */
266
+ .kerusi-toolbar {
267
+ --kc-size: var(--kerusi-control-size, 2rem);
268
+ --kc-bg: var(--kerusi-control-bg, var(--kerusi-control-bg-input));
269
+ --kc-fg: var(--kerusi-control-fg, var(--kerusi-control-fg-input));
270
+ --kc-border: var(
271
+ --kerusi-control-border,
272
+ color-mix(in srgb, var(--kerusi-control-border-input) 40%, transparent)
273
+ );
274
+ display: flex;
275
+ align-self: flex-end;
276
+ /* Stays put under a maximized map's scrolling stage. */
277
+ flex: none;
278
+ box-sizing: border-box;
279
+ border: 1px solid var(--kc-border);
280
+ border-radius: var(--kerusi-control-radius, 0.5rem);
281
+ overflow: hidden;
282
+ box-shadow: var(--kerusi-control-shadow, 0 1px 3px rgb(0 0 0 / 0.14));
283
+ }
284
+
285
+ /* A fingertip needs more than a cursor does: keep the 44px target on touch. */
286
+ @media (pointer: coarse) {
287
+ .kerusi-toolbar {
288
+ --kc-size: var(--kerusi-control-size, 2.75rem);
289
+ }
290
+ }
291
+
292
+ .kerusi-control {
293
+ display: inline-flex;
294
+ align-items: center;
295
+ justify-content: center;
296
+ box-sizing: border-box;
297
+ width: var(--kc-size);
298
+ height: var(--kc-size);
299
+ padding: 0;
300
+ border: 0;
301
+ border-radius: 0;
302
+ background: var(--kc-bg);
303
+ color: var(--kc-fg);
304
+ font: inherit;
305
+ cursor: pointer;
306
+ transition: background-color 120ms ease-out;
307
+ }
308
+
309
+ .kerusi-control + .kerusi-control {
310
+ border-inline-start: 1px solid var(--kc-border);
311
+ }
312
+
313
+ .kerusi-control:hover {
314
+ background: var(--kerusi-control-hover-bg, color-mix(in srgb, var(--kc-fg) 10%, var(--kc-bg)));
315
+ }
316
+
317
+ .kerusi-control:active {
318
+ background: var(--kerusi-control-hover-bg, color-mix(in srgb, var(--kc-fg) 18%, var(--kc-bg)));
319
+ }
320
+
321
+ /* `aria-disabled`, not `disabled`: a disabled button drops out of the tab order,
322
+ and pressing "zoom in" until the ceiling would throw focus away mid-press. */
323
+ .kerusi-control[aria-disabled='true'] {
324
+ opacity: 0.4;
325
+ cursor: default;
326
+ background: var(--kc-bg);
327
+ }
328
+
329
+ .kerusi-control svg {
330
+ width: var(--kerusi-control-icon-size, 1rem);
331
+ height: var(--kerusi-control-icon-size, 1rem);
332
+ fill: none;
333
+ stroke: currentColor;
334
+ stroke-width: var(--kerusi-control-icon-stroke, 1.75);
335
+ stroke-linecap: round;
336
+ stroke-linejoin: round;
337
+ pointer-events: none;
338
+ }
339
+
340
+ /* Inset, so the bar's `overflow: hidden` does not clip it. */
341
+ .kerusi-control:focus-visible {
342
+ outline: 2px solid var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));
343
+ outline-offset: -2px;
344
+ }
345
+
346
+ .kerusi-viewport:focus-visible {
347
+ outline: 2px solid var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));
348
+ outline-offset: 2px;
349
+ }
350
+
351
+ /* A window onto the map. `clip` rather than `hidden`: a hidden box can still be
352
+ scrolled by the browser when a seat inside takes focus, which would fight the
353
+ transform; a clipped one cannot be scrolled at all. */
354
+ .kerusi-viewport {
355
+ position: relative;
356
+ overflow: hidden;
357
+ overflow: clip;
358
+ user-select: none;
359
+ -webkit-user-select: none;
360
+ }
361
+
362
+ /* The same column the map sits in outside a viewport. */
363
+ .kerusi-viewport__content {
364
+ display: flex;
365
+ flex-direction: column;
366
+ gap: 1.25rem;
367
+ transform-origin: 0 0;
368
+ }
369
+
370
+ /* Only for a button, key or reveal. A finger or wheel moves the map directly, and
371
+ easing behind it would feel like lag. */
372
+ .kerusi-viewport__content--animated {
373
+ transition: transform 160ms ease-out;
374
+ }
375
+
376
+ @media (prefers-reduced-motion: reduce) {
377
+ .kerusi-viewport__content--animated {
378
+ transition: none;
379
+ }
380
+ }
381
+
382
+ /* Maximized: the map fills the screen. This is the whole feature on iPhone,
383
+ which has no element full screen; elsewhere native full screen is requested
384
+ on top. `Canvas` / `CanvasText` and the backdrop come from inline styles. */
385
+ .kerusi-seatmap--maximized {
386
+ position: fixed;
387
+ inset: 0;
388
+ z-index: var(--kerusi-maximized-z-index, 1000);
389
+ box-sizing: border-box;
390
+ padding: 0.75rem;
391
+ gap: 0.5rem;
392
+ overflow: hidden;
393
+ overscroll-behavior: contain;
394
+ }
395
+
396
+ .kerusi-seatmap--maximized .kerusi-seatmap__stage {
397
+ flex: 1 1 auto;
398
+ min-height: 0;
399
+ gap: 0.5rem;
400
+ /* A map that is not zoomable has no viewport to pan, so it scrolls instead. */
401
+ overflow: auto;
402
+ }
403
+
404
+ .kerusi-seatmap--maximized .kerusi-viewport {
405
+ flex: 1 1 auto;
406
+ min-height: 0;
407
+ container-type: size;
408
+ }
409
+
410
+ /* Fit the whole canvas to the screen: as wide as the viewport allows, or as
411
+ wide as its height allows at the canvas's proportions, whichever is less.
412
+ `--kerusi-canvas-aspect` is the canvas's width over its height, set inline. */
413
+ .kerusi-seatmap--maximized .kerusi-viewport__content {
414
+ width: min(100cqw, 100cqh * var(--kerusi-canvas-aspect, 1));
415
+ }
416
+
417
+ /* --- seat cards -------------------------------------------------------------
418
+ Only when `renderHoverCard` or `renderClickCard` is set; a map without them
419
+ renders none of these elements. The card is positioned by the component
420
+ (`left`, `top`, `data-placement`); this file owns what it looks like by
421
+ default, entirely through custom properties, so a consumer restyles it from
422
+ their own stylesheet without `!important` (D11):
423
+
424
+ --kerusi-seat-card-bg, -fg, -border, -radius, -shadow, -padding, -max-width,
425
+ -font, -offset, -arrow-size, -z-index
426
+
427
+ None is set here on `.kerusi-seatmap`, so a value set on the map or any
428
+ ancestor wins. The same rules as the Angular component's stylesheet. */
429
+
430
+ :where(.kerusi-seat-card) {
431
+ position: absolute;
432
+ z-index: var(--kerusi-seat-card-z-index, 10);
433
+ box-sizing: border-box;
434
+ max-width: min(var(--kerusi-seat-card-max-width, 280px), 100%);
435
+ padding: var(--kerusi-seat-card-padding, 8px 12px);
436
+ border: var(--kerusi-seat-card-border, 1px solid rgb(0 0 0 / 0.12));
437
+ border-radius: var(--kerusi-seat-card-radius, 8px);
438
+ background: var(--kerusi-seat-card-bg, var(--kerusi-element-bg, #fff));
439
+ color: var(--kerusi-seat-card-fg, var(--kerusi-element-fg, #1a1d24));
440
+ box-shadow: var(--kerusi-seat-card-shadow, 0 4px 14px rgb(0 0 0 / 0.16));
441
+ font: var(--kerusi-seat-card-font, inherit);
442
+ text-align: start;
443
+ /* Text in a card can be selected and its links followed. */
444
+ user-select: text;
445
+ -webkit-user-select: text;
446
+ animation: kerusi-seat-card-in 120ms ease-out;
447
+ }
448
+
449
+ :where(.kerusi-seat-card):focus-visible {
450
+ outline: 2px solid var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));
451
+ outline-offset: 2px;
452
+ }
453
+
454
+ :where(.kerusi-seat-card__body) {
455
+ min-width: 0;
456
+ overflow-wrap: anywhere;
457
+ }
458
+
459
+ /* No chrome: the consumer's content is the card. Only positioning is left. */
460
+ :where(.kerusi-seat-card--bare) {
461
+ padding: 0;
462
+ border: 0;
463
+ border-radius: 0;
464
+ background: none;
465
+ box-shadow: none;
466
+ }
467
+
468
+ /* The arrow is a square turned 45 degrees and centred on the card's edge that
469
+ faces the seat, so half of it shows. `--kerusi-seat-card-arrow-shift` is how
470
+ far along that edge, set inline from the placement. Its background covers the
471
+ card's own border where they meet; only the two sides that point out are
472
+ drawn, from the same border token as the card. The component leaves the arrow
473
+ out when `--kerusi-seat-card-arrow-size` is `0`. */
474
+ :where(.kerusi-seat-card__arrow) {
475
+ position: absolute;
476
+ box-sizing: border-box;
477
+ width: calc(var(--kerusi-seat-card-arrow-size, 6px) * 1.4142);
478
+ height: calc(var(--kerusi-seat-card-arrow-size, 6px) * 1.4142);
479
+ background: inherit;
480
+ pointer-events: none;
481
+ }
482
+
483
+ /* The card is above the seat: the arrow is on its bottom edge, pointing down. */
484
+ :where(.kerusi-seat-card[data-placement='top'] .kerusi-seat-card__arrow) {
485
+ bottom: 0;
486
+ left: var(--kerusi-seat-card-arrow-shift);
487
+ transform: translate(-50%, 50%) rotate(45deg);
488
+ border-right: var(--kerusi-seat-card-border, 1px solid rgb(0 0 0 / 0.12));
489
+ border-bottom: var(--kerusi-seat-card-border, 1px solid rgb(0 0 0 / 0.12));
490
+ }
491
+
492
+ :where(.kerusi-seat-card[data-placement='bottom'] .kerusi-seat-card__arrow) {
493
+ top: 0;
494
+ left: var(--kerusi-seat-card-arrow-shift);
495
+ transform: translate(-50%, -50%) rotate(45deg);
496
+ border-top: var(--kerusi-seat-card-border, 1px solid rgb(0 0 0 / 0.12));
497
+ border-left: var(--kerusi-seat-card-border, 1px solid rgb(0 0 0 / 0.12));
498
+ }
499
+
500
+ :where(.kerusi-seat-card[data-placement='left'] .kerusi-seat-card__arrow) {
501
+ right: 0;
502
+ top: var(--kerusi-seat-card-arrow-shift);
503
+ transform: translate(50%, -50%) rotate(45deg);
504
+ border-top: var(--kerusi-seat-card-border, 1px solid rgb(0 0 0 / 0.12));
505
+ border-right: var(--kerusi-seat-card-border, 1px solid rgb(0 0 0 / 0.12));
506
+ }
507
+
508
+ :where(.kerusi-seat-card[data-placement='right'] .kerusi-seat-card__arrow) {
509
+ left: 0;
510
+ top: var(--kerusi-seat-card-arrow-shift);
511
+ transform: translate(-50%, -50%) rotate(45deg);
512
+ border-bottom: var(--kerusi-seat-card-border, 1px solid rgb(0 0 0 / 0.12));
513
+ border-left: var(--kerusi-seat-card-border, 1px solid rgb(0 0 0 / 0.12));
514
+ }
515
+
516
+ /* A short fade and settle, from the side the card grows out of. */
517
+ :where(.kerusi-seat-card[data-placement='top']) {
518
+ transform-origin: bottom center;
519
+ }
520
+ :where(.kerusi-seat-card[data-placement='bottom']) {
521
+ transform-origin: top center;
522
+ }
523
+ :where(.kerusi-seat-card[data-placement='left']) {
524
+ transform-origin: center right;
525
+ }
526
+ :where(.kerusi-seat-card[data-placement='right']) {
527
+ transform-origin: center left;
528
+ }
529
+
530
+ @keyframes kerusi-seat-card-in {
531
+ from {
532
+ opacity: 0;
533
+ transform: scale(0.96);
534
+ }
535
+ }
536
+
537
+ @media (prefers-reduced-motion: reduce) {
538
+ :where(.kerusi-seat-card) {
539
+ animation: none;
540
+ }
541
+ }