@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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,49 @@
1
+ import type { DeltaApplication, HeldSeat, KerusiState, KerusiStateDelta, SequenceReader } from '@kerusiweb/core';
2
+ export interface UseKerusiStateOptions {
3
+ now?: () => Date;
4
+ sequenceOf?: SequenceReader;
5
+ /**
6
+ * Run {@link UseKerusiState.tick} on this interval, reverting lapsed holds
7
+ * and refreshing the `heldSeats` countdowns. Omit to tick manually.
8
+ */
9
+ expiryIntervalMs?: number;
10
+ }
11
+ export interface UseKerusiState {
12
+ /** The current availability snapshot. */
13
+ state: KerusiState;
14
+ /** The timestamp of the most recently applied document. */
15
+ updatedAt: string;
16
+ /**
17
+ * True once a gap was detected in the delta stream. The consumer SHOULD
18
+ * re-fetch a full `KerusiState` and hand it to {@link reset} (§5.2).
19
+ */
20
+ needsRefetch: boolean;
21
+ /** Every currently-held seat, with the time left on its hold. */
22
+ heldSeats: readonly HeldSeat[];
23
+ /** Offers a delta to the store. Returns what happened to it. */
24
+ apply(delta: KerusiStateDelta): DeltaApplication;
25
+ /**
26
+ * Replaces the state wholesale — the post-refetch path. Clears the refetch
27
+ * flag and forgets the delta sequence.
28
+ */
29
+ reset(state: KerusiState): void;
30
+ /** Reverts lapsed holds and refreshes the `heldSeats` countdowns. */
31
+ tick(): void;
32
+ }
33
+ /**
34
+ * A live view of a {@link KerusiState}, fed by deltas from whatever transport
35
+ * the application uses.
36
+ *
37
+ * The ordering, gap detection and hold expiry all come from `@kerusiweb/core`;
38
+ * this hook only holds them in React state. It is deliberately not a context or
39
+ * a provider: this is a rendering library, and an application that already owns
40
+ * its availability state should keep owning it.
41
+ *
42
+ * ```tsx
43
+ * const availability = useKerusiState(initialState, { expiryIntervalMs: 1000 });
44
+ * useEffect(() => socket.on('delta', availability.apply), [availability.apply]);
45
+ * <KerusiSeatmap map={map} state={availability.state} />
46
+ * ```
47
+ */
48
+ export declare function useKerusiState(initial: KerusiState, opts?: UseKerusiStateOptions): UseKerusiState;
49
+ //# sourceMappingURL=use-kerusi-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-kerusi-state.d.ts","sourceRoot":"","sources":["../src/use-kerusi-state.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,gBAAgB,EAChB,cAAc,EACf,MAAM,iBAAiB,CAAC;AAGzB,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,KAAK,EAAE,WAAW,CAAC;IACnB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB,iEAAiE;IACjE,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC/B,gEAAgE;IAChE,KAAK,CAAC,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,CAAC;IACjD;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,qEAAqE;IACrE,IAAI,IAAI,IAAI,CAAC;CACd;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,WAAW,EACpB,IAAI,GAAE,qBAA0B,GAC/B,cAAc,CA4EhB"}
@@ -0,0 +1,82 @@
1
+ import { applyStateDeltaOrdered, DEFAULT_SEQUENCE_READER, expireHolds, heldSeats as heldSeatsIn, } from '@kerusiweb/core';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ /**
4
+ * A live view of a {@link KerusiState}, fed by deltas from whatever transport
5
+ * the application uses.
6
+ *
7
+ * The ordering, gap detection and hold expiry all come from `@kerusiweb/core`;
8
+ * this hook only holds them in React state. It is deliberately not a context or
9
+ * a provider: this is a rendering library, and an application that already owns
10
+ * its availability state should keep owning it.
11
+ *
12
+ * ```tsx
13
+ * const availability = useKerusiState(initialState, { expiryIntervalMs: 1000 });
14
+ * useEffect(() => socket.on('delta', availability.apply), [availability.apply]);
15
+ * <KerusiSeatmap map={map} state={availability.state} />
16
+ * ```
17
+ */
18
+ export function useKerusiState(initial, opts = {}) {
19
+ const { now, sequenceOf, expiryIntervalMs } = opts;
20
+ const clock = useRef(now ?? (() => new Date()));
21
+ clock.current = now ?? clock.current;
22
+ const readSequence = sequenceOf ?? DEFAULT_SEQUENCE_READER;
23
+ const [state, setState] = useState(initial);
24
+ const [needsRefetch, setNeedsRefetch] = useState(false);
25
+ const [nowValue, setNowValue] = useState(() => clock.current());
26
+ const lastSequence = useRef(undefined);
27
+ /** Mirrors `state` so `apply` can stay referentially stable. */
28
+ const latest = useRef(state);
29
+ latest.current = state;
30
+ const apply = useCallback((delta) => {
31
+ const result = applyStateDeltaOrdered(latest.current, delta, {
32
+ sequenceOf: readSequence,
33
+ lastSequence: lastSequence.current,
34
+ });
35
+ if (result.outcome === 'applied' || result.outcome === 'gap') {
36
+ latest.current = result.state;
37
+ setState(result.state);
38
+ const seq = readSequence(delta);
39
+ if (seq !== undefined) {
40
+ lastSequence.current = seq;
41
+ }
42
+ }
43
+ if (result.outcome === 'gap') {
44
+ setNeedsRefetch(true);
45
+ }
46
+ return result;
47
+ }, [readSequence]);
48
+ const reset = useCallback((next) => {
49
+ latest.current = next;
50
+ setState(next);
51
+ setNeedsRefetch(false);
52
+ lastSequence.current = undefined;
53
+ setNowValue(clock.current());
54
+ }, []);
55
+ const tick = useCallback(() => {
56
+ const at = clock.current();
57
+ setNowValue(at);
58
+ const next = expireHolds(latest.current, at);
59
+ if (next !== latest.current) {
60
+ latest.current = next;
61
+ setState(next);
62
+ }
63
+ }, []);
64
+ useEffect(() => {
65
+ if (expiryIntervalMs === undefined) {
66
+ return;
67
+ }
68
+ const handle = setInterval(tick, expiryIntervalMs);
69
+ return () => clearInterval(handle);
70
+ }, [expiryIntervalMs, tick]);
71
+ const held = useMemo(() => heldSeatsIn(state, nowValue), [state, nowValue]);
72
+ return {
73
+ state,
74
+ updatedAt: state.updatedAt,
75
+ needsRefetch,
76
+ heldSeats: held,
77
+ apply,
78
+ reset,
79
+ tick,
80
+ };
81
+ }
82
+ //# sourceMappingURL=use-kerusi-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-kerusi-state.js","sourceRoot":"","sources":["../src/use-kerusi-state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { RenderMap } from '@kerusiweb/core';
2
+ export interface RovingFocus {
3
+ /** The seat holding each section's tab stop, keyed by section id. */
4
+ focusedSeatIds: Readonly<Record<string, string>>;
5
+ /** Records the tab stop without moving DOM focus — for a focus that already happened. */
6
+ setFocusedSeat(sectionId: string, seatId: string): void;
7
+ /** Moves the tab stop and the DOM focus that follows it. */
8
+ moveFocusTo(sectionId: string, seatId: string): void;
9
+ /** Focuses a seat wherever it is, if it is currently rendered. */
10
+ focusSeat(seatId: string): void;
11
+ /** Ref callback each seat `<g>` registers itself through. */
12
+ registerSeat(seatId: string, node: SVGGElement | null): void;
13
+ }
14
+ /**
15
+ * The roving tab stop, held per section rather than per map.
16
+ *
17
+ * Arrow keys never cross a section boundary, so a single map-wide tab stop
18
+ * would leave every section after the first unreachable by keyboard. Tab moves
19
+ * between sections, arrows move within.
20
+ *
21
+ * Seats register their DOM node here as they mount, which is what replaces the
22
+ * Angular binding's `querySelectorAll` scan: seat ids come from the document,
23
+ * so they cannot be interpolated into a selector unescaped, and `CSS.escape` is
24
+ * absent in jsdom and under SSR. A ref map has neither problem.
25
+ */
26
+ export declare function useRovingFocus(renderMap: RenderMap): RovingFocus;
27
+ //# sourceMappingURL=use-roving-focus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-roving-focus.d.ts","sourceRoot":"","sources":["../src/use-roving-focus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,yFAAyF;IACzF,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACxD,4DAA4D;IAC5D,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACrD,kEAAkE;IAClE,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,6DAA6D;IAC7D,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC;CAC9D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,WAAW,CA8DhE"}
@@ -0,0 +1,66 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ /**
3
+ * The roving tab stop, held per section rather than per map.
4
+ *
5
+ * Arrow keys never cross a section boundary, so a single map-wide tab stop
6
+ * would leave every section after the first unreachable by keyboard. Tab moves
7
+ * between sections, arrows move within.
8
+ *
9
+ * Seats register their DOM node here as they mount, which is what replaces the
10
+ * Angular binding's `querySelectorAll` scan: seat ids come from the document,
11
+ * so they cannot be interpolated into a selector unescaped, and `CSS.escape` is
12
+ * absent in jsdom and under SSR. A ref map has neither problem.
13
+ */
14
+ export function useRovingFocus(renderMap) {
15
+ const [focusedSeatIds, setFocusedSeatIds] = useState({});
16
+ const nodes = useRef(new Map());
17
+ /** Set when a keyboard move asks for focus, consumed after the commit. */
18
+ const pendingFocus = useRef(null);
19
+ // Keep every section's tab stop pointing at a seat that still exists.
20
+ useEffect(() => {
21
+ setFocusedSeatIds((current) => {
22
+ const next = {};
23
+ let changed = Object.keys(current).length !== renderMap.sections.length;
24
+ for (const section of renderMap.sections) {
25
+ const held = current[section.id];
26
+ const valid = held && section.seats.some((seat) => seat.id === held);
27
+ const resolved = valid ? held : section.seats[0]?.id;
28
+ if (resolved) {
29
+ next[section.id] = resolved;
30
+ changed ||= resolved !== held;
31
+ }
32
+ }
33
+ return changed ? next : current;
34
+ });
35
+ }, [renderMap]);
36
+ const registerSeat = useCallback((seatId, node) => {
37
+ if (node) {
38
+ nodes.current.set(seatId, node);
39
+ }
40
+ else {
41
+ nodes.current.delete(seatId);
42
+ }
43
+ }, []);
44
+ const focusSeat = useCallback((seatId) => {
45
+ nodes.current.get(seatId)?.focus?.();
46
+ }, []);
47
+ const setFocusedSeat = useCallback((sectionId, seatId) => {
48
+ setFocusedSeatIds((all) => (all[sectionId] === seatId ? all : { ...all, [sectionId]: seatId }));
49
+ }, []);
50
+ const moveFocusTo = useCallback((sectionId, seatId) => {
51
+ // The target's `tabindex` only becomes 0 on the next commit, so defer the
52
+ // focus until then — focusing a `tabindex="-1"` node works, but leaves
53
+ // the tab stop and the focus ring momentarily disagreeing.
54
+ pendingFocus.current = seatId;
55
+ setFocusedSeat(sectionId, seatId);
56
+ }, [setFocusedSeat]);
57
+ useEffect(() => {
58
+ const target = pendingFocus.current;
59
+ if (target !== null) {
60
+ pendingFocus.current = null;
61
+ nodes.current.get(target)?.focus?.();
62
+ }
63
+ }, [focusedSeatIds]);
64
+ return { focusedSeatIds, setFocusedSeat, moveFocusTo, focusSeat, registerSeat };
65
+ }
66
+ //# sourceMappingURL=use-roving-focus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-roving-focus.js","sourceRoot":"","sources":["../src/use-roving-focus.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@kerusiweb/react",
3
+ "version": "2.0.0",
4
+ "description": "A React seat-map component that renders Kerusi Seat Map & Availability Format documents as interactive, accessible inline SVG.",
5
+ "keywords": [
6
+ "react",
7
+ "kerusi",
8
+ "seat-map",
9
+ "seatmap",
10
+ "seat-picker",
11
+ "seat-selection",
12
+ "svg",
13
+ "booking",
14
+ "ticketing",
15
+ "cinema",
16
+ "airline",
17
+ "venue"
18
+ ],
19
+ "license": "MIT",
20
+ "author": "Arsyad \"Shad Ahm\" Ahmad",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/ShadAhm/kerusiweb.git",
24
+ "directory": "projects/react"
25
+ },
26
+ "homepage": "https://github.com/ShadAhm/kerusiweb",
27
+ "type": "module",
28
+ "sideEffects": [
29
+ "*.css"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/public-api.d.ts",
34
+ "default": "./dist/public-api.js"
35
+ },
36
+ "./styles.css": "./styles.css",
37
+ "./package.json": "./package.json"
38
+ },
39
+ "types": "./dist/public-api.d.ts",
40
+ "main": "./dist/public-api.js",
41
+ "files": [
42
+ "dist",
43
+ "styles.css"
44
+ ],
45
+ "peerDependencies": {
46
+ "@kerusiweb/core": "^2.0.0",
47
+ "react": "^18.3.0 || ^19.0.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }
package/styles.css ADDED
@@ -0,0 +1,230 @@
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
+ .kerusi-element__void {
152
+ stroke-width: 1;
153
+ stroke-dasharray: 4 3;
154
+ opacity: 0.7;
155
+ }
156
+
157
+ /* --- the legend ---------------------------------------------------------- */
158
+
159
+ .kerusi-legend {
160
+ display: flex;
161
+ flex-wrap: wrap;
162
+ gap: 1.5rem;
163
+ font-size: 0.85rem;
164
+ }
165
+
166
+ .kerusi-legend__block {
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: 0.4rem;
170
+ min-width: 0;
171
+ }
172
+
173
+ .kerusi-legend__heading {
174
+ margin: 0;
175
+ font-size: 0.72rem;
176
+ font-weight: 600;
177
+ letter-spacing: 0.06em;
178
+ text-transform: uppercase;
179
+ opacity: 0.65;
180
+ }
181
+
182
+ .kerusi-legend__list {
183
+ display: flex;
184
+ flex-wrap: wrap;
185
+ gap: 0.35rem 0.9rem;
186
+ margin: 0;
187
+ padding: 0;
188
+ list-style: none;
189
+ }
190
+
191
+ .kerusi-legend__item {
192
+ display: inline-flex;
193
+ align-items: center;
194
+ gap: 0.4rem;
195
+ }
196
+
197
+ .kerusi-legend__swatch {
198
+ display: inline-block;
199
+ flex: none;
200
+ width: 0.85rem;
201
+ height: 0.85rem;
202
+ border-radius: 3px;
203
+ }
204
+
205
+ /* The seat glyph carries its own corner radius in the viewBox. */
206
+ .kerusi-legend__swatch--glyph {
207
+ border-radius: 0;
208
+ overflow: visible;
209
+ }
210
+
211
+ /* Mirrors `.kerusi-seat__occupant--*` on the map, so the key matches what it
212
+ is keying. Fill/stroke colour is bound inline; these set the weight. */
213
+ .kerusi-legend__occupant--selected {
214
+ opacity: 0.2;
215
+ }
216
+
217
+ .kerusi-legend__occupant--booked {
218
+ opacity: 0.55;
219
+ }
220
+
221
+ .kerusi-legend__occupant--held {
222
+ opacity: 0.75;
223
+ stroke-linecap: round;
224
+ stroke-linejoin: round;
225
+ }
226
+
227
+ .kerusi-legend__price {
228
+ opacity: 0.7;
229
+ font-variant-numeric: tabular-nums;
230
+ }