@sbb-esta/lyne-elements-dev 5.11.0-dev.1791971551 → 5.11.0-dev.1791974623

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,605 @@
1
+ import { __esDecorate, __runInitializers } from "tslib";
2
+ import { html, nothing, svg, unsafeCSS } from "lit";
3
+ import { property } from "lit/decorators.js";
4
+ import { classMap } from "lit/directives/class-map.js";
5
+ import { ref } from "lit/directives/ref.js";
6
+ import { SbbElement, SbbPropertyWatcherController, defaultDateAdapter, forceType } from "./core.js";
7
+ import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
8
+ import { ResizeController } from "@lit-labs/observers/resize-controller.js";
9
+
10
+ //#region src/elements/pearl-chain/pearl-chain-node/pearl-chain-node.scss?inline
11
+ var pearl_chain_node_default = ":host {\n display: flex;\n position: relative;\n width: var(--sbb-pearl-chain-node-size);\n height: var(--sbb-pearl-chain-node-size);\n align-items: center;\n justify-content: center;\n}\n\n.bullet {\n width: var(--sbb-pearl-chain-bullet-size);\n height: var(--sbb-pearl-chain-bullet-size);\n color: var(--sbb-pearl-chain-bullet-color);\n rotate: var(--sbb-pearl-chain-bullet-rotation, 0deg);\n}\n.bullet circle {\n cx: 50%;\n cy: 50%;\n}\n\n.bullet--past {\n --sbb-pearl-chain-bullet-color: var(--sbb-pearl-chain-bullet-past-color);\n}\n\n.bullet--disruption {\n --sbb-pearl-chain-bullet-color: var(--sbb-pearl-chain-bullet-disruption-color);\n}\n\n.bullet--irrelevant {\n --sbb-pearl-chain-bullet-color: var(--sbb-pearl-chain-bullet-irrelevant-color);\n --sbb-pearl-chain-bullet-stop-stroke-width: var(--sbb-pearl-chain-irrelevant-stroke-width);\n}\n\n.bullet--stop {\n width: var(--sbb-pearl-chain-bullet-stop-size);\n height: var(--sbb-pearl-chain-bullet-stop-size);\n}\n.bullet--stop circle {\n fill: transparent;\n r: calc(50% - var(--sbb-pearl-chain-bullet-stop-stroke-width) / 2);\n stroke-width: var(--sbb-pearl-chain-bullet-stop-stroke-width);\n}\n\n.bullet--skip .dash {\n fill: var(--sbb-color-error);\n}\n\n.bullet--exceptional-skip {\n width: var(--sbb-pearl-chain-bullet-exceptional-skip-size);\n height: var(--sbb-pearl-chain-bullet-exceptional-skip-size);\n}\n.bullet--exceptional-skip .dash {\n fill: var(--sbb-color-error);\n}\n\n.bullet--stop-duty .outer-circle {\n fill: transparent;\n r: calc(50% - var(--sbb-pearl-chain-bullet-stroke-width) / 2);\n stroke-width: var(--sbb-pearl-chain-bullet-stroke-width);\n stroke-dasharray: 2.1 1;\n}\n\n.bullet--boarding .outer-circle,\n.bullet--alighting .outer-circle,\n.bullet--exceptional-boarding .outer-circle,\n.bullet--exceptional-alighting .outer-circle,\n.bullet--stop-on-demand .outer-circle,\n.bullet--boarding-on-demand .outer-circle,\n.bullet--alighting-on-demand .outer-circle {\n fill: transparent;\n r: calc(50% - var(--sbb-pearl-chain-bullet-stroke-width) / 2);\n stroke-width: var(--sbb-pearl-chain-bullet-stroke-width);\n}\n";
12
+
13
+ //#endregion
14
+ //#region src/elements/pearl-chain/pearl-chain-node/pearl-chain-node.component.ts
15
+ const parseTriState = (value) => value === "arrival" || value === "departure" ? value : value === "" || value === true ? true : null;
16
+ /**
17
+ * Represents a station/point of interest within a `sbb-pearl-chain`.
18
+ */
19
+ let SbbPearlChainNodeElement = (() => {
20
+ let _classSuper = SbbElement;
21
+ let _instanceExtraInitializers = [];
22
+ let _type_decorators;
23
+ let _type_initializers = [];
24
+ let _type_extraInitializers = [];
25
+ let _disrupted_decorators;
26
+ let _disrupted_initializers = [];
27
+ let _disrupted_extraInitializers = [];
28
+ let _walk_decorators;
29
+ let _walk_initializers = [];
30
+ let _walk_extraInitializers = [];
31
+ let _unsure_decorators;
32
+ let _unsure_initializers = [];
33
+ let _unsure_extraInitializers = [];
34
+ let _irrelevant_decorators;
35
+ let _irrelevant_initializers = [];
36
+ let _irrelevant_extraInitializers = [];
37
+ let _set_arrival_decorators;
38
+ let _set_departure_decorators;
39
+ return class SbbPearlChainNodeElement extends _classSuper {
40
+ static {
41
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
42
+ _type_decorators = [property()];
43
+ _disrupted_decorators = [forceType(parseTriState), property()];
44
+ _walk_decorators = [forceType(parseTriState), property()];
45
+ _unsure_decorators = [forceType(parseTriState), property()];
46
+ _irrelevant_decorators = [forceType(parseTriState), property()];
47
+ _set_arrival_decorators = [property()];
48
+ _set_departure_decorators = [property()];
49
+ __esDecorate(this, null, _type_decorators, {
50
+ kind: "accessor",
51
+ name: "type",
52
+ static: false,
53
+ private: false,
54
+ access: {
55
+ has: (obj) => "type" in obj,
56
+ get: (obj) => obj.type,
57
+ set: (obj, value) => {
58
+ obj.type = value;
59
+ }
60
+ },
61
+ metadata: _metadata
62
+ }, _type_initializers, _type_extraInitializers);
63
+ __esDecorate(this, null, _disrupted_decorators, {
64
+ kind: "accessor",
65
+ name: "disrupted",
66
+ static: false,
67
+ private: false,
68
+ access: {
69
+ has: (obj) => "disrupted" in obj,
70
+ get: (obj) => obj.disrupted,
71
+ set: (obj, value) => {
72
+ obj.disrupted = value;
73
+ }
74
+ },
75
+ metadata: _metadata
76
+ }, _disrupted_initializers, _disrupted_extraInitializers);
77
+ __esDecorate(this, null, _walk_decorators, {
78
+ kind: "accessor",
79
+ name: "walk",
80
+ static: false,
81
+ private: false,
82
+ access: {
83
+ has: (obj) => "walk" in obj,
84
+ get: (obj) => obj.walk,
85
+ set: (obj, value) => {
86
+ obj.walk = value;
87
+ }
88
+ },
89
+ metadata: _metadata
90
+ }, _walk_initializers, _walk_extraInitializers);
91
+ __esDecorate(this, null, _unsure_decorators, {
92
+ kind: "accessor",
93
+ name: "unsure",
94
+ static: false,
95
+ private: false,
96
+ access: {
97
+ has: (obj) => "unsure" in obj,
98
+ get: (obj) => obj.unsure,
99
+ set: (obj, value) => {
100
+ obj.unsure = value;
101
+ }
102
+ },
103
+ metadata: _metadata
104
+ }, _unsure_initializers, _unsure_extraInitializers);
105
+ __esDecorate(this, null, _irrelevant_decorators, {
106
+ kind: "accessor",
107
+ name: "irrelevant",
108
+ static: false,
109
+ private: false,
110
+ access: {
111
+ has: (obj) => "irrelevant" in obj,
112
+ get: (obj) => obj.irrelevant,
113
+ set: (obj, value) => {
114
+ obj.irrelevant = value;
115
+ }
116
+ },
117
+ metadata: _metadata
118
+ }, _irrelevant_initializers, _irrelevant_extraInitializers);
119
+ __esDecorate(this, null, _set_arrival_decorators, {
120
+ kind: "setter",
121
+ name: "arrival",
122
+ static: false,
123
+ private: false,
124
+ access: {
125
+ has: (obj) => "arrival" in obj,
126
+ set: (obj, value) => {
127
+ obj.arrival = value;
128
+ }
129
+ },
130
+ metadata: _metadata
131
+ }, null, _instanceExtraInitializers);
132
+ __esDecorate(this, null, _set_departure_decorators, {
133
+ kind: "setter",
134
+ name: "departure",
135
+ static: false,
136
+ private: false,
137
+ access: {
138
+ has: (obj) => "departure" in obj,
139
+ set: (obj, value) => {
140
+ obj.departure = value;
141
+ }
142
+ },
143
+ metadata: _metadata
144
+ }, null, _instanceExtraInitializers);
145
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
146
+ enumerable: true,
147
+ configurable: true,
148
+ writable: true,
149
+ value: _metadata
150
+ });
151
+ }
152
+ static {
153
+ this.elementName = "sbb-pearl-chain-node";
154
+ }
155
+ static {
156
+ this.styles = unsafeCSS(pearl_chain_node_default);
157
+ }
158
+ #type_accessor_storage;
159
+ /** Decides how the bullet is rendered. Defaults to _empty_ (no bullet). */
160
+ get type() {
161
+ return this.#type_accessor_storage;
162
+ }
163
+ set type(value) {
164
+ this.#type_accessor_storage = value;
165
+ }
166
+ #disrupted_accessor_storage;
167
+ /** Marks the node as disrupted, coloring the bullet red (if rendered). */
168
+ get disrupted() {
169
+ return this.#disrupted_accessor_storage;
170
+ }
171
+ set disrupted(value) {
172
+ this.#disrupted_accessor_storage = value;
173
+ }
174
+ #walk_accessor_storage;
175
+ /** Marks the node as a walk connection. Does not affect the bullet color. */
176
+ get walk() {
177
+ return this.#walk_accessor_storage;
178
+ }
179
+ set walk(value) {
180
+ this.#walk_accessor_storage = value;
181
+ }
182
+ #unsure_accessor_storage;
183
+ /** Marks the connection as unsure, rendering the line dashed. Does not affect the bullet. */
184
+ get unsure() {
185
+ return this.#unsure_accessor_storage;
186
+ }
187
+ set unsure(value) {
188
+ this.#unsure_accessor_storage = value;
189
+ }
190
+ #irrelevant_accessor_storage;
191
+ /** Marks the node as irrelevant, coloring the bullet gray (if rendered); supersedes `disrupted`. */
192
+ get irrelevant() {
193
+ return this.#irrelevant_accessor_storage;
194
+ }
195
+ set irrelevant(value) {
196
+ this.#irrelevant_accessor_storage = value;
197
+ }
198
+ /** The arrival date/time at this node. Accepts ISO 8601 datetime strings. */
199
+ set arrival(value) {
200
+ this._arrival = defaultDateAdapter.getValidDateOrNull(defaultDateAdapter.deserialize(value));
201
+ }
202
+ get arrival() {
203
+ return this._arrival;
204
+ }
205
+ /** The departure date/time at this node. Accepts ISO 8601 datetime strings. */
206
+ set departure(value) {
207
+ this._departure = defaultDateAdapter.getValidDateOrNull(defaultDateAdapter.deserialize(value));
208
+ }
209
+ get departure() {
210
+ return this._departure;
211
+ }
212
+ get now() {
213
+ return this._chain?.now ?? this._now;
214
+ }
215
+ constructor() {
216
+ super();
217
+ this.#type_accessor_storage = (__runInitializers(this, _instanceExtraInitializers), __runInitializers(this, _type_initializers, null));
218
+ this.#disrupted_accessor_storage = (__runInitializers(this, _type_extraInitializers), __runInitializers(this, _disrupted_initializers, null));
219
+ this.#walk_accessor_storage = (__runInitializers(this, _disrupted_extraInitializers), __runInitializers(this, _walk_initializers, null));
220
+ this.#unsure_accessor_storage = (__runInitializers(this, _walk_extraInitializers), __runInitializers(this, _unsure_initializers, null));
221
+ this.#irrelevant_accessor_storage = (__runInitializers(this, _unsure_extraInitializers), __runInitializers(this, _irrelevant_initializers, null));
222
+ this._arrival = (__runInitializers(this, _irrelevant_extraInitializers), null);
223
+ this._departure = null;
224
+ this._now = /* @__PURE__ */ new Date();
225
+ this._chain = null;
226
+ this.addController(new SbbPropertyWatcherController(this, () => this.closest("sbb-pearl-chain"), { now: () => this.requestUpdate() }));
227
+ }
228
+ connectedCallback() {
229
+ super.connectedCallback();
230
+ this._chain = this.closest("sbb-pearl-chain");
231
+ this._chain?.addNode(this);
232
+ }
233
+ disconnectedCallback() {
234
+ super.disconnectedCallback();
235
+ this._chain?.removeNode(this);
236
+ this._chain = null;
237
+ }
238
+ updated(changedProperties) {
239
+ super.updated(changedProperties);
240
+ if (changedProperties.size > 0) this._chain?.requestUpdate();
241
+ }
242
+ _isPast() {
243
+ return !!this.departure && this.departure.getTime() < this.now.getTime();
244
+ }
245
+ _renderBullet() {
246
+ switch (this.type) {
247
+ case "start":
248
+ case "end":
249
+ case "commercial":
250
+ case "exceptional": return svg`
251
+ <circle r="50%" fill="currentColor" />
252
+ `;
253
+ case "stop": return svg`
254
+ <circle
255
+ stroke="currentColor"
256
+ />
257
+ `;
258
+ case "skip": return svg`
259
+ <path d="m3.65 2.59c-.36.36-.59.86-.59 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l1.42 1.42c-.73.72-1.72 1.17-2.83 1.17-2.21 0-4-1.79-4-4 0-1.1.45-2.1 1.17-2.83l1.42 1.42zm1.41-2.59c2.21 0 4 1.79 4 4 0 .55-.11 1.08-.32 1.56l-1.69-1.69c-.06-1-.86-1.8-1.87-1.87l-1.68-1.68c.48-.21 1.01-.32 1.56-.32z" fill="currentColor"/>
260
+ <path class="dash" d="m1.06 0 7.78 7.78-1.06 1.06-7.78-7.78 1.06-1.06z"/>
261
+ `;
262
+ case "exceptional-skip": return svg`
263
+ <path d="m9.5 8.5c-.9.9-2.1 1.5-3.5 1.5-2.7 0-5-2.2-5-5 0-1.4.6-2.6 1.5-3.5l7 7zm-3.5-8.5c2.8 0 5 2.2 5 5 0 .8-.2 1.6-.6 2.3l-6.7-6.7c.7-.4 1.5-.6 2.3-.6z" fill="currentColor"/>
264
+ <rect class="dash" x="1.41431" width="14" height="2" transform="rotate(45 1.41431 0)"/>
265
+ `;
266
+ case "stop-duty": return svg`
267
+ <circle
268
+ class="outer-circle"
269
+ stroke="currentColor"
270
+ />
271
+ `;
272
+ case "boarding":
273
+ case "exceptional-boarding": return svg`
274
+ <circle
275
+ class="outer-circle"
276
+ stroke="currentColor"
277
+ />
278
+ <path d="m1.85 5c0 .84.34 1.64.92 2.23.59.58 1.39.92 2.23.92.84 0 1.64-.34 2.23-.92.58-.59.92-1.39.92-2.23l-3.15 0-3.15 0z" fill="currentColor"/>
279
+ `;
280
+ case "alighting":
281
+ case "exceptional-alighting": return svg`
282
+ <circle
283
+ class="outer-circle"
284
+ stroke="currentColor"
285
+ />
286
+ <path d="m8.15 5c0-.84-.34-1.64-.92-2.23-.59-.58-1.39-.92-2.23-.92-.84 0-1.64.34-2.23.92-.58.59-.92 1.39-.92 2.23l3.15 0 3.15 0z" fill="currentColor"/>
287
+ `;
288
+ case "stop-on-demand": return svg`
289
+ <circle
290
+ class="outer-circle"
291
+ stroke="currentColor"
292
+ />
293
+ <circle
294
+ fill="currentColor"
295
+ r="20%"
296
+ />
297
+ `;
298
+ case "boarding-on-demand": return svg`
299
+ <circle
300
+ class="outer-circle"
301
+ stroke="currentColor"
302
+ />
303
+ <path d="m3 5c0 .53.21 1.04.59 1.41.37.38.88.59 1.41.59.53 0 1.04-.21 1.41-.59.38-.37.59-.88.59-1.41l-2 0h-2z" fill="currentColor"/>
304
+ `;
305
+ case "alighting-on-demand": return svg`
306
+ <circle
307
+ class="outer-circle"
308
+ stroke="currentColor"
309
+ />
310
+ <path d="m7 5c0-.53-.21-1.04-.59-1.41-.37-.38-.88-.59-1.41-.59-.53 0-1.04.21-1.41.59-.38.37-.59.88-.59 1.41l2 0h2z" fill="currentColor"/>
311
+ `;
312
+ default: return nothing;
313
+ }
314
+ }
315
+ render() {
316
+ return html`
317
+ <svg
318
+ viewBox=${this.type === "exceptional-skip" ? "0 0 12 12" : "0 0 10 10"}
319
+ class="bullet ${classMap({
320
+ [`bullet--${this.type}`]: !!this.type,
321
+ "bullet--irrelevant": !!this.irrelevant,
322
+ "bullet--disruption": !!this.disrupted,
323
+ "bullet--past": this._isPast()
324
+ })}"
325
+ aria-hidden="true"
326
+ ${ref((el) => {
327
+ this.svgElem = el;
328
+ })}
329
+ >
330
+ ${this._renderBullet()}
331
+ </svg>
332
+ `;
333
+ }
334
+ };
335
+ })();
336
+
337
+ //#endregion
338
+ //#region src/elements/pearl-chain/pearl-chain/pearl-chain.scss?inline
339
+ var pearl_chain_default = ":host {\n position: relative;\n display: block;\n --sbb-pearl-chain-bullet-now-animation-duration: var(--sbb-disable-animation-duration, 2s);\n}\n\n:host(:is(:state(horizontal),[state--horizontal])) {\n --sbb-pearl-chain-bullet-rotation: -90deg;\n}\n\n.sbb-pearl-chain__svg {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n overflow: visible;\n pointer-events: none;\n}\n\n.line {\n stroke: var(--sbb-pearl-chain-color);\n stroke-width: var(--sbb-pearl-chain-stroke-width);\n}\n\n.line--past {\n stroke: var(--sbb-pearl-chain-past-color);\n}\n\n.line--walk {\n stroke: var(--sbb-pearl-chain-walk-color);\n stroke-dasharray: 0.125rem;\n}\n\n.line--unsure {\n stroke-dasharray: 0.125rem;\n}\n\n.line--disruption {\n stroke: var(--sbb-pearl-chain-disruption-color);\n stroke-dasharray: 0.125rem;\n}\n\n.line--irrelevant {\n stroke: var(--sbb-pearl-chain-irrelevant-color);\n stroke-width: var(--sbb-pearl-chain-irrelevant-stroke-width);\n}\n\n.now-dot {\n fill: var(--sbb-pearl-chain-now-color);\n stroke: var(--sbb-pearl-chain-now-color);\n stroke-opacity: var(--sbb-pearl-chain-bullet-now-animation-opacity);\n r: calc(var(--sbb-pearl-chain-now-size) / 2);\n animation: sbb-pearl-chain-pulse var(--sbb-pearl-chain-bullet-now-animation-duration) ease-out infinite;\n}\n\n@keyframes sbb-pearl-chain-pulse {\n 0% {\n stroke-width: 0;\n }\n 50% {\n stroke-width: calc(var(--sbb-pearl-chain-now-size) / 2);\n }\n 100% {\n stroke-width: 0;\n }\n}\n";
340
+
341
+ //#endregion
342
+ //#region src/elements/pearl-chain/pearl-chain/pearl-chain.component.ts
343
+ /** Gap (in px) left before a node's bullet, so the line doesn't touch it. */
344
+ const NODE_GAP = 2;
345
+ /** Interval (in ms) at which the component is re-rendered. */
346
+ const RENDERING_INTERVAL = 3e4;
347
+ const DEBOUNCE_TIME = 150;
348
+ /**
349
+ * Linearly interpolates between two points.
350
+ */
351
+ const lerp = (from, to, ratio) => ({
352
+ x: from.x + (to.x - from.x) * ratio,
353
+ y: from.y + (to.y - from.y) * ratio
354
+ });
355
+ /** Pulls `to` back towards `from` by `gap` px, along the line's direction. */
356
+ const pullBack = (from, to, gap) => {
357
+ const length = Math.hypot(to.x - from.x, to.y - from.y);
358
+ return length === 0 ? to : lerp(from, to, Math.max(0, (length - gap) / length));
359
+ };
360
+ /**
361
+ * It displays a chain of `sbb-pearl-chain-node` elements connected by lines,
362
+ * representing a journey with its stops.
363
+ */
364
+ let SbbPearlChainElement = (() => {
365
+ let _classSuper = SbbElement;
366
+ let _instanceExtraInitializers = [];
367
+ let _set_now_decorators;
368
+ return class SbbPearlChainElement extends _classSuper {
369
+ static {
370
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
371
+ _set_now_decorators = [property()];
372
+ __esDecorate(this, null, _set_now_decorators, {
373
+ kind: "setter",
374
+ name: "now",
375
+ static: false,
376
+ private: false,
377
+ access: {
378
+ has: (obj) => "now" in obj,
379
+ set: (obj, value) => {
380
+ obj.now = value;
381
+ }
382
+ },
383
+ metadata: _metadata
384
+ }, null, _instanceExtraInitializers);
385
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
386
+ enumerable: true,
387
+ configurable: true,
388
+ writable: true,
389
+ value: _metadata
390
+ });
391
+ }
392
+ static {
393
+ this.elementName = "sbb-pearl-chain";
394
+ }
395
+ static {
396
+ this.styles = unsafeCSS(pearl_chain_default);
397
+ }
398
+ /**
399
+ * A configured date which acts as the current date instead of the real current date.
400
+ * Only recommended for testing purposes.
401
+ */
402
+ set now(value) {
403
+ this._userNow = defaultDateAdapter.getValidDateOrNull(defaultDateAdapter.deserialize(value));
404
+ }
405
+ get now() {
406
+ return this._userNow ?? this._systemNow;
407
+ }
408
+ /** Returns the registered `sbb-pearl-chain-node` elements, in DOM order. */
409
+ get nodes() {
410
+ return this._nodes.slice();
411
+ }
412
+ constructor() {
413
+ super();
414
+ this._userNow = (__runInitializers(this, _instanceExtraInitializers), null);
415
+ this._systemNow = /* @__PURE__ */ new Date();
416
+ this._nodes = [];
417
+ this._orientation = "vertical";
418
+ const controllersConfig = {
419
+ callback: () => this._debouncedPositionLines(),
420
+ skipInitial: true
421
+ };
422
+ this.addController(new ResizeController(this, controllersConfig));
423
+ this.addController(new IntersectionController(this, controllersConfig));
424
+ }
425
+ /**
426
+ * Returns the relative position (0 for the first node, 1 for the last one) of each node,
427
+ * in DOM order, proportional to its `departure` time (or `arrival` if there is no `departure`).
428
+ */
429
+ calculateNodeWeights() {
430
+ const times = this._nodes.map((n) => (n.departure ?? n.arrival).getTime());
431
+ const first = times[0];
432
+ const range = times[times.length - 1] - first;
433
+ return times.map((t) => (t - first) / range);
434
+ }
435
+ /**
436
+ * Registers a `sbb-pearl-chain-node` with this pearl chain.
437
+ * @internal
438
+ */
439
+ addNode(node) {
440
+ if (this._nodes.includes(node)) return;
441
+ this._nodes.push(node);
442
+ this._nodes.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
443
+ this.requestUpdate();
444
+ }
445
+ /**
446
+ * Unregisters a `sbb-pearl-chain-node` from this pearl chain.
447
+ * @internal
448
+ */
449
+ removeNode(node) {
450
+ const index = this._nodes.indexOf(node);
451
+ if (index === -1) return;
452
+ this._nodes.splice(index, 1);
453
+ this.requestUpdate();
454
+ }
455
+ connectedCallback() {
456
+ super.connectedCallback();
457
+ this._systemNowIntervalId = setInterval(() => {
458
+ this._systemNow = /* @__PURE__ */ new Date();
459
+ this.requestUpdate();
460
+ }, RENDERING_INTERVAL);
461
+ }
462
+ disconnectedCallback() {
463
+ super.disconnectedCallback();
464
+ clearInterval(this._systemNowIntervalId);
465
+ }
466
+ updated(changedProperties) {
467
+ super.updated(changedProperties);
468
+ this._positionLines();
469
+ }
470
+ firstUpdated(changedProperties) {
471
+ super.firstUpdated(changedProperties);
472
+ this._svgElem = this.shadowRoot.querySelector(".sbb-pearl-chain__svg");
473
+ }
474
+ /**
475
+ * Optimizes `_positionLines()` calls by debouncing them (called by observers).
476
+ */
477
+ _debouncedPositionLines() {
478
+ clearTimeout(this._debounceId);
479
+ this._debounceId = setTimeout(() => this._positionLines(), DEBOUNCE_TIME);
480
+ }
481
+ /**
482
+ * Positions the rendered lines (and dot) to connect the bullet nodes, and toggles
483
+ * `:is(:state(horizontal),[state--horizontal])` based on whether nodes are spread more horizontally than vertically.
484
+ */
485
+ _positionLines() {
486
+ const elements = this.shadowRoot?.querySelectorAll("[data-segment]");
487
+ if (!elements?.length || !this._svgElem) return;
488
+ const svgRect = this._svgElem.getBoundingClientRect();
489
+ const nodesCords = this._nodes.map((node) => {
490
+ const rect = node["svgElem"]?.getBoundingClientRect() ?? node.getBoundingClientRect();
491
+ return {
492
+ x: rect.left + rect.width / 2 - svgRect.left,
493
+ y: rect.top + rect.height / 2 - svgRect.top,
494
+ size: rect.width
495
+ };
496
+ });
497
+ this._updateOrientation(nodesCords);
498
+ for (const el of elements) {
499
+ const index = Number(el.dataset.segment);
500
+ const start = pullBack(nodesCords[index + 1], nodesCords[index], nodesCords[index].size / 2);
501
+ const end = this._needsGap(this._nodes[index + 1]) ? pullBack(start, nodesCords[index + 1], NODE_GAP + nodesCords[index + 1].size / 2) : pullBack(start, nodesCords[index + 1], nodesCords[index + 1].size / 2);
502
+ switch (el.dataset.role) {
503
+ case "dot": {
504
+ const ratio = this._progressRatio(this._nodes[index], this._nodes[index + 1]);
505
+ const nowCords = lerp(nodesCords[index], nodesCords[index + 1], ratio);
506
+ el.setAttribute("cx", `${nowCords.x}`);
507
+ el.setAttribute("cy", `${nowCords.y}`);
508
+ break;
509
+ }
510
+ case "before": {
511
+ const ratio = this._progressRatio(this._nodes[index], this._nodes[index + 1]);
512
+ const nowCords = lerp(start, end, ratio);
513
+ this._setLineCords(el, start, nowCords);
514
+ break;
515
+ }
516
+ case "after": {
517
+ const ratio = this._progressRatio(this._nodes[index], this._nodes[index + 1]);
518
+ const nowCords = lerp(start, end, ratio);
519
+ this._setLineCords(el, nowCords, end);
520
+ break;
521
+ }
522
+ default: this._setLineCords(el, start, end);
523
+ }
524
+ }
525
+ }
526
+ /** Sets the coordinates of a line svg element. */
527
+ _setLineCords(element, start, end) {
528
+ element.setAttribute("x1", `${start.x}`);
529
+ element.setAttribute("y1", `${start.y}`);
530
+ element.setAttribute("x2", `${end.x}`);
531
+ element.setAttribute("y2", `${end.y}`);
532
+ }
533
+ /** Toggles `:is(:state(horizontal),[state--horizontal])` when the nodes are spread more horizontally than vertically. */
534
+ _updateOrientation(nodesCords) {
535
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
536
+ for (const { x, y } of nodesCords) {
537
+ if (x < minX) minX = x;
538
+ if (x > maxX) maxX = x;
539
+ if (y < minY) minY = y;
540
+ if (y > maxY) maxY = y;
541
+ }
542
+ const horizontal = maxX - minX > maxY - minY;
543
+ this.toggleState("horizontal", horizontal);
544
+ this._orientation = horizontal ? "horizontal" : "vertical";
545
+ }
546
+ /** No gap for `start` or `end` bullets. */
547
+ _needsGap(node) {
548
+ return this._orientation === "horizontal" && node.type !== null && node.type !== "start" && node.type !== "end";
549
+ }
550
+ /**
551
+ * Ratio (0-1) of "now" between the line's start departure and end arrival, or
552
+ * `null` if "now" is outside that window (or either date is missing).
553
+ */
554
+ _progressRatio(start, end) {
555
+ const from = start.departure;
556
+ const to = end.arrival;
557
+ if (!from || !to) return null;
558
+ const now = this.now.getTime();
559
+ if (now < from.getTime() || now > to.getTime()) return null;
560
+ return (now - from.getTime()) / (to.getTime() - from.getTime());
561
+ }
562
+ /**
563
+ * Returns a map of CSS classes for a line segment between two nodes, based on its state.
564
+ */
565
+ _lineClasses(start, end) {
566
+ return {
567
+ "line--past": this._isLinePast(end),
568
+ "line--walk": start.walk === true || start.walk === "departure" || end.walk === true || end.walk === "arrival",
569
+ "line--unsure": start.unsure === true || start.unsure === "departure" || end.unsure === true || end.unsure === "arrival",
570
+ "line--irrelevant": start.irrelevant === true || start.irrelevant === "departure" || end.irrelevant === true || end.irrelevant === "arrival",
571
+ "line--disruption": start.disrupted === true || start.disrupted === "departure" || end.disrupted === true || end.disrupted === "arrival"
572
+ };
573
+ }
574
+ /** A line is "past" once we've already arrived at its ending node. */
575
+ _isLinePast(end) {
576
+ const pastTime = end.arrival ?? end.departure;
577
+ return !!pastTime && pastTime.getTime() < this.now.getTime();
578
+ }
579
+ /** A line is "current" if "now" falls between its start departure and end arrival. */
580
+ _isLineCurrent(start, end) {
581
+ return this._progressRatio(start, end) !== null;
582
+ }
583
+ /** Renders one segment: a single line, or (when "now" falls within it) a gray/normal split + pulsing dot. */
584
+ _renderSegment(start, end, index) {
585
+ const classes = this._lineClasses(start, end);
586
+ if (!classes["line--disruption"] && !classes["line--walk"] && this._isLineCurrent(start, end)) return svg`
587
+ <line class="line line--past" data-segment="${index}" data-role="before"></line>
588
+ <line class="line ${classMap(classes)}" data-segment="${index}" data-role="after"></line>
589
+ <circle class="now-dot" data-segment="${index}" data-role="dot"></circle>
590
+ `;
591
+ return svg`<line class="line ${classMap(classes)}" data-segment="${index}"></line>`;
592
+ }
593
+ render() {
594
+ return html`
595
+ <slot></slot>
596
+ <svg class="sbb-pearl-chain__svg" aria-hidden="true">
597
+ ${this._nodes.length > 1 ? Array.from({ length: this._nodes.length - 1 }, (_, index) => this._renderSegment(this._nodes[index], this._nodes[index + 1], index)) : nothing}
598
+ </svg>
599
+ `;
600
+ }
601
+ };
602
+ })();
603
+
604
+ //#endregion
605
+ export { SbbPearlChainElement, SbbPearlChainNodeElement };
package/index.d.ts CHANGED
@@ -45,6 +45,7 @@ import { SbbNotificationElement } from "./notification.js";
45
45
  import { SbbOptGroupElement, SbbOptionElement, SbbOptionHintElement } from "./option.js";
46
46
  import { SbbOverlayElement } from "./overlay.js";
47
47
  import { SbbCompactPaginatorElement, SbbPaginatorElement } from "./paginator.js";
48
+ import { SbbPearlChainElement, SbbPearlChainNodeElement } from "./pearl-chain.js";
48
49
  import { SbbPopoverCloseButtonElement, SbbPopoverElement } from "./popover.js";
49
50
  import { SbbRadioButtonElement } from "./radio-button.js";
50
51
  import { SbbRadioButtonGroupElement } from "./radio-button-group.js";
@@ -192,6 +193,8 @@ declare global {
192
193
  var SbbOptionHintElement: SbbOptionHintElement;
193
194
  var SbbOverlayElement: SbbOverlayElement;
194
195
  var SbbPaginatorElement: SbbPaginatorElement;
196
+ var SbbPearlChainElement: SbbPearlChainElement;
197
+ var SbbPearlChainNodeElement: SbbPearlChainNodeElement;
195
198
  var SbbPopoverCloseButtonElement: SbbPopoverCloseButtonElement;
196
199
  var SbbPopoverElement: SbbPopoverElement;
197
200
  var SbbRadioButtonElement: SbbRadioButtonElement;
package/index.js CHANGED
@@ -45,6 +45,7 @@ import { SbbNotificationElement } from "./notification.js";
45
45
  import { SbbOptGroupElement, SbbOptionElement, SbbOptionHintElement } from "./option.js";
46
46
  import { SbbOverlayElement } from "./overlay.js";
47
47
  import { SbbCompactPaginatorElement, SbbPaginatorElement } from "./paginator.js";
48
+ import { SbbPearlChainElement, SbbPearlChainNodeElement } from "./pearl-chain.js";
48
49
  import { SbbPopoverCloseButtonElement, SbbPopoverElement } from "./popover.js";
49
50
  import { SbbRadioButtonElement } from "./radio-button.js";
50
51
  import { SbbRadioButtonGroupElement } from "./radio-button-group.js";
@@ -192,6 +193,8 @@ globalThis.SbbOptionElement = SbbOptionElement;
192
193
  globalThis.SbbOptionHintElement = SbbOptionHintElement;
193
194
  globalThis.SbbOverlayElement = SbbOverlayElement;
194
195
  globalThis.SbbPaginatorElement = SbbPaginatorElement;
196
+ globalThis.SbbPearlChainElement = SbbPearlChainElement;
197
+ globalThis.SbbPearlChainNodeElement = SbbPearlChainNodeElement;
195
198
  globalThis.SbbPopoverCloseButtonElement = SbbPopoverCloseButtonElement;
196
199
  globalThis.SbbPopoverElement = SbbPopoverElement;
197
200
  globalThis.SbbRadioButtonElement = SbbRadioButtonElement;
@@ -1741,6 +1741,28 @@ summary {
1741
1741
  --sbb-selection-panel-suffix-color: var(--sbb-color-3);
1742
1742
  --sbb-selection-panel-subtext-color: var(--sbb-color-granite);
1743
1743
  --sbb-selection-panel-subtext-color: light-dark(var(--sbb-color-granite), var(--sbb-color-smoke));
1744
+ --sbb-pearl-chain-color: var(--sbb-color-3);
1745
+ --sbb-pearl-chain-stroke-width: var(--sbb-border-width-2x);
1746
+ --sbb-pearl-chain-past-color: var(--sbb-color-metal);
1747
+ --sbb-pearl-chain-past-color: light-dark(var(--sbb-color-metal), var(--sbb-color-granite));
1748
+ --sbb-pearl-chain-irrelevant-color: var(--sbb-color-metal);
1749
+ --sbb-pearl-chain-irrelevant-color: light-dark(var(--sbb-color-metal), var(--sbb-color-granite));
1750
+ --sbb-pearl-chain-irrelevant-stroke-width: var(--sbb-border-width-1x);
1751
+ --sbb-pearl-chain-disruption-color: var(--sbb-color-error);
1752
+ --sbb-pearl-chain-walk-color: var(--sbb-color-sky);
1753
+ --sbb-pearl-chain-now-color: var(--sbb-color-primary);
1754
+ --sbb-pearl-chain-now-size: var(--sbb-pearl-chain-bullet-size);
1755
+ --sbb-pearl-chain-node-size: 0.75em;
1756
+ --sbb-pearl-chain-bullet-size: 0.625em;
1757
+ --sbb-pearl-chain-bullet-stop-size: 0.5em;
1758
+ --sbb-pearl-chain-bullet-stop-stroke-width: var(--sbb-pearl-chain-stroke-width);
1759
+ --sbb-pearl-chain-bullet-exceptional-skip-size: 0.75em;
1760
+ --sbb-pearl-chain-bullet-color: var(--sbb-pearl-chain-color);
1761
+ --sbb-pearl-chain-bullet-past-color: var(--sbb-pearl-chain-past-color);
1762
+ --sbb-pearl-chain-bullet-irrelevant-color: var(--sbb-pearl-chain-irrelevant-color);
1763
+ --sbb-pearl-chain-bullet-disruption-color: var(--sbb-color-error);
1764
+ --sbb-pearl-chain-bullet-stroke-width: var(--sbb-pearl-chain-stroke-width);
1765
+ --sbb-pearl-chain-bullet-now-animation-opacity: 0.2;
1744
1766
  --sbb-popover-arrow-size: var(--sbb-spacing-fixed-4x);
1745
1767
  --sbb-popover-max-width: calc(100% - var(--sbb-spacing-fixed-2x));
1746
1768
  --sbb-popover-border-radius: var(--sbb-border-radius-8x);