@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.121 → 2.0.0-next.122

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.
Files changed (39) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +233 -53
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +66 -17
  4. package/dist/automation/automation-tank/automation-tank.css.js +94 -14
  5. package/dist/automation/automation-tank/automation-tank.css.js.map +1 -1
  6. package/dist/automation/automation-tank/automation-tank.d.ts +53 -5
  7. package/dist/automation/automation-tank/automation-tank.d.ts.map +1 -1
  8. package/dist/automation/automation-tank/automation-tank.js +39 -20
  9. package/dist/automation/automation-tank/automation-tank.js.map +1 -1
  10. package/dist/building-blocks/readout-block/readout-block.d.ts +27 -1
  11. package/dist/building-blocks/readout-block/readout-block.d.ts.map +1 -1
  12. package/dist/building-blocks/readout-block/readout-block.js +31 -10
  13. package/dist/building-blocks/readout-block/readout-block.js.map +1 -1
  14. package/dist/navigation-instruments/readout/readout-formatters.d.ts +61 -0
  15. package/dist/navigation-instruments/readout/readout-formatters.d.ts.map +1 -1
  16. package/dist/navigation-instruments/readout/readout-formatters.js +35 -2
  17. package/dist/navigation-instruments/readout/readout-formatters.js.map +1 -1
  18. package/dist/navigation-instruments/readout/readout.d.ts +19 -2
  19. package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
  20. package/dist/navigation-instruments/readout/readout.js +18 -4
  21. package/dist/navigation-instruments/readout/readout.js.map +1 -1
  22. package/dist/navigation-instruments/readout-list/readout-list.d.ts.map +1 -1
  23. package/dist/navigation-instruments/readout-list/readout-list.js +6 -3
  24. package/dist/navigation-instruments/readout-list/readout-list.js.map +1 -1
  25. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts +19 -2
  26. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
  27. package/dist/navigation-instruments/readout-list-item/readout-list-item.js +18 -4
  28. package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/automation/automation-tank/automation-tank.css +70 -3
  31. package/src/automation/automation-tank/automation-tank.stories.ts +140 -1
  32. package/src/automation/automation-tank/automation-tank.ts +106 -27
  33. package/src/building-blocks/readout-block/readout-block.stories.ts +20 -0
  34. package/src/building-blocks/readout-block/readout-block.ts +56 -10
  35. package/src/navigation-instruments/readout/readout-formatters.spec.ts +205 -0
  36. package/src/navigation-instruments/readout/readout-formatters.ts +128 -2
  37. package/src/navigation-instruments/readout/readout.ts +38 -5
  38. package/src/navigation-instruments/readout-list/readout-list.ts +10 -2
  39. package/src/navigation-instruments/readout-list-item/readout-list-item.ts +38 -5
@@ -21,6 +21,115 @@ export function isReadoutValueType(value: unknown): value is ReadoutValueType {
21
21
  return typeof value === 'string' && READOUT_VALUE_TYPES.includes(value);
22
22
  }
23
23
 
24
+ /**
25
+ * The largest `fractionDigits` `Number.prototype.toFixed` accepts. Reused as the
26
+ * `maxDigits` ceiling so the two bounds stay symmetric — a wider reserve than
27
+ * this is meaningless anyway, and an unbounded one builds a huge string.
28
+ */
29
+ export const READOUT_MAX_DIGITS = 100;
30
+
31
+ /**
32
+ * `fractionDigits` as `toFixed` will actually read it: `NaN` and an unset value
33
+ * count as 0, and a fractional count truncates.
34
+ */
35
+ function effectiveFractionDigits(
36
+ fractionDigits: number | null | undefined
37
+ ): number {
38
+ // `toFixed(undefined)` behaves as `toFixed(0)`, and the components read
39
+ // `fractionDigits ?? 0`, so an unset value must not be rejected here — the
40
+ // assertion has to accept exactly what the runtime accepts.
41
+ if (fractionDigits === null || fractionDigits === undefined) {
42
+ return 0;
43
+ }
44
+ return Number.isNaN(fractionDigits) ? 0 : Math.trunc(fractionDigits);
45
+ }
46
+
47
+ /**
48
+ * Throws when `fractionDigits` is outside the range `toFixed` accepts.
49
+ *
50
+ * It is a public property that reaches `Number.prototype.toFixed` unchanged, so
51
+ * `-1`, `101` or `Infinity` already throw a bare
52
+ * `RangeError: toFixed() digits argument must be between 0 and 100` from inside
53
+ * the update cycle. This replaces that with an error naming the component and
54
+ * the offending value.
55
+ *
56
+ * Deliberately not clamped: `fractionDigits` sets the PRECISION of a reading, so
57
+ * quietly turning `-1` into `0` would drop decimals from a displayed value
58
+ * without telling anyone. It is a configuration mistake, fixable only in code —
59
+ * the same class as a bad `valueType`, and treated the same way.
60
+ *
61
+ * Values `toFixed` itself tolerates are left alone: `NaN` reads as `0`, and a
62
+ * fractional count truncates (`2.7` → `2`).
63
+ */
64
+ export function assertReadoutFractionDigits(
65
+ tagName: string,
66
+ fractionDigits: number | null | undefined
67
+ ): void {
68
+ const digits = effectiveFractionDigits(fractionDigits);
69
+ if (Number.isFinite(digits) && digits >= 0 && digits <= READOUT_MAX_DIGITS) {
70
+ return;
71
+ }
72
+ throw new RangeError(
73
+ // `String`, not `JSON.stringify`: the latter serialises `Infinity` and
74
+ // `NaN` to `null`, hiding the very value being rejected.
75
+ `<${tagName}>: fractionDigits must be between 0 and ${READOUT_MAX_DIGITS} ` +
76
+ `(got ${String(fractionDigits)}).`
77
+ );
78
+ }
79
+
80
+ /**
81
+ * A digit count used for WIDTH RESERVATION, normalised to a non-negative integer
82
+ * no greater than {@link READOUT_MAX_DIGITS}.
83
+ *
84
+ * Deliberately named for the role rather than for `maxDigits`: both `maxDigits`
85
+ * and the fraction count `obc-readout-list` reserves with are the same kind of
86
+ * input — a count handed to `String.prototype.repeat` — and must be bounded the
87
+ * same way. `fractionDigits` as a FORMAT input is a different contract and is
88
+ * rejected instead; see {@link assertReadoutFractionDigits}.
89
+ *
90
+ * Clamped rather than rejected because it only reserves width, so bounding it
91
+ * changes no reading's meaning, and the surrounding code already absorbs bad
92
+ * values silently (`Math.max(count, 1)`, `NaN` repeating to `''`). What is not
93
+ * absorbed is `Infinity`, which throws out of `String.prototype.repeat`, and a
94
+ * large finite count, which builds a reserver string long enough to matter —
95
+ * both are capped here.
96
+ */
97
+ export function resolveReadoutDigitCount(
98
+ count: number | null | undefined
99
+ ): number {
100
+ const digits = Math.trunc(count ?? 0);
101
+ if (Number.isNaN(digits) || digits <= 0) {
102
+ // Matches what the existing guards already do with these: reserve nothing.
103
+ return 0;
104
+ }
105
+ // `Infinity` lands on the cap — "as wide as possible" — rather than throwing.
106
+ return Math.min(digits, READOUT_MAX_DIGITS);
107
+ }
108
+
109
+ /**
110
+ * Whether a digit-count knob failed to arrive as a number: `null`, `undefined`
111
+ * or `NaN`.
112
+ *
113
+ * A missing knob renders the READING unavailable (the dash) rather than
114
+ * silently formatting with a default the author never chose. `fractionDigits`
115
+ * is typically written by the consuming system, and a runtime failure there
116
+ * produces exactly these values — formatting with `0` instead would print a
117
+ * critical `0.4` as a plausible-looking `0`, which an operator cannot tell
118
+ * apart from a healthy reading. The dash makes the failure visible.
119
+ *
120
+ * This is a third contract besides throw and clamp: a finite out-of-range
121
+ * `fractionDigits` is a programmer error and throws
122
+ * ({@link assertReadoutFractionDigits}); a finite out-of-range `maxDigits` is
123
+ * width-only and clamps ({@link resolveReadoutDigitCount}); a knob that never
124
+ * arrived is a runtime data condition and dashes the reading, the same class
125
+ * as a `NaN` value.
126
+ */
127
+ export function isReadoutDigitCountMissing(
128
+ count: number | null | undefined
129
+ ): boolean {
130
+ return count === null || count === undefined || Number.isNaN(count);
131
+ }
132
+
24
133
  function isBlank(value: string): boolean {
25
134
  return value.trim() === '';
26
135
  }
@@ -144,10 +253,18 @@ export const READOUT_UNAVAILABLE_DASH = '\u2012';
144
253
  function dashedGenerator({
145
254
  showZeroPadding,
146
255
  minValueLength,
147
- fractionDigits,
256
+ fractionDigits: rawFractionDigits,
148
257
  }: ReadoutNumericFormatOptions): string {
149
258
  const visibleDigits = showZeroPadding ? Math.max(minValueLength, 1) : 1;
150
259
 
260
+ // A missing precision shapes the placeholder as zero fraction digits — a
261
+ // single dash. Without this, `NaN < 1` is false, both `repeat(NaN)` calls
262
+ // below produce the empty string, and the placeholder degenerates to a
263
+ // lone `"."`.
264
+ const fractionDigits = Number.isNaN(rawFractionDigits)
265
+ ? 0
266
+ : rawFractionDigits;
267
+
151
268
  if (fractionDigits < 1) {
152
269
  return READOUT_UNAVAILABLE_DASH.repeat(visibleDigits);
153
270
  }
@@ -169,7 +286,16 @@ export function formatNumericValue(
169
286
  // `resolveReadoutNumericValue`. Every caller normalises today, but this
170
287
  // function is exported, and `NaN.toFixed()` would put the literal text
171
288
  // "NaN" where a reading belongs — the exact failure this change removes.
172
- if (value === undefined || !Number.isFinite(value)) {
289
+ //
290
+ // A missing precision (`NaN` fractionDigits) is the same class of failure
291
+ // from the other operand: `value.toFixed(NaN)` silently formats with zero
292
+ // decimals, printing a critical `0.4` as a plausible-looking `0`. The
293
+ // reading is untrustworthy without its precision, so it dashes too.
294
+ if (
295
+ value === undefined ||
296
+ !Number.isFinite(value) ||
297
+ Number.isNaN(options.fractionDigits)
298
+ ) {
173
299
  return dashedGenerator(options);
174
300
  }
175
301
 
@@ -27,6 +27,9 @@ import {
27
27
  import {Priority} from '../types.js';
28
28
  import {
29
29
  assertReadoutValueType,
30
+ assertReadoutFractionDigits,
31
+ isReadoutDigitCountMissing,
32
+ resolveReadoutDigitCount,
30
33
  resolveReadoutNumericValue,
31
34
  ReadoutValueType,
32
35
  type ReadoutNumericFormatOptions,
@@ -288,13 +291,22 @@ export class ObcReadout extends LitElement {
288
291
  @property({type: Boolean}) hasDegreeSpacer = false;
289
292
  /**
290
293
  * Also formats the numeric setpoint / advice blocks, which stay numeric
291
- * even when the value is text.
294
+ * even when the value is text. Must be between 0 and 100 — the range
295
+ * `Number.prototype.toFixed` accepts; outside it throws a `RangeError`.
296
+ * A fractional count truncates (`2.7` → `2`). A count that never arrived
297
+ * (`NaN`, `null` or unset) renders the reading as the unavailable dash —
298
+ * formatting with a precision the author never chose would let a critical
299
+ * `0.4` pass for a healthy `0`.
292
300
  * @availableWhen valueType==number || hasSetpoint==true || hasAdvice==true
293
301
  */
294
302
  @property({type: Number}) fractionDigits = 0;
295
303
  /**
296
304
  * Also formats the numeric setpoint / advice blocks, which stay numeric
297
- * even when the value is text.
305
+ * even when the value is text. Bounded before use: a fractional count
306
+ * truncates (`2.7` → `2`), anything above 100 — including `Infinity` —
307
+ * caps at 100, and a negative count reserves nothing. A count that never
308
+ * arrived (`NaN`, `null` or unset) renders the reading as the unavailable
309
+ * dash, consistent with `fractionDigits`.
298
310
  * @availableWhen valueType==number || hasSetpoint==true || hasAdvice==true
299
311
  */
300
312
  @property({type: Number}) maxDigits = 0;
@@ -375,7 +387,21 @@ export class ObcReadout extends LitElement {
375
387
  }
376
388
 
377
389
  private get resolvedMaxDigits(): number {
378
- return this.maxDigits ?? 0;
390
+ return resolveReadoutDigitCount(this.maxDigits);
391
+ }
392
+
393
+ /**
394
+ * Whether a digit knob failed to arrive (`NaN` / `null` / `undefined`).
395
+ * The raw knobs are forwarded to the blocks, which render the reading as
396
+ * the unavailable dash (see `obc-readout-block.digitCountsMissing`); this
397
+ * getter only keeps the setpoint comparison in agreement with what is
398
+ * displayed.
399
+ */
400
+ private get digitCountsMissing(): boolean {
401
+ return (
402
+ isReadoutDigitCountMissing(this.fractionDigits) ||
403
+ isReadoutDigitCountMissing(this.maxDigits)
404
+ );
379
405
  }
380
406
 
381
407
  private get hasSrc(): boolean {
@@ -390,6 +416,12 @@ export class ObcReadout extends LitElement {
390
416
  if (!this.hasSetpoint) {
391
417
  return false;
392
418
  }
419
+ // A missing digit knob renders every numeric block as the dash; two
420
+ // dashes must not read as "at the setpoint", so the comparison stays off
421
+ // entirely while the configuration is untrustworthy.
422
+ if (this.digitCountsMissing) {
423
+ return false;
424
+ }
393
425
  // A text value never compares equal to a setpoint, so flip-flop / pop-up
394
426
  // stay dormant for `valueType="text"`.
395
427
  return isDisplayedAtSetpoint(
@@ -558,8 +590,8 @@ export class ObcReadout extends LitElement {
558
590
  .weight=${config.weight}
559
591
  .hasDegree=${config.hasDegree ?? false}
560
592
  .hasIcon=${config.hasIcon ?? false}
561
- .fractionDigits=${this.resolvedFractionDigits}
562
- .maxDigits=${this.resolvedMaxDigits}
593
+ .fractionDigits=${this.fractionDigits}
594
+ .maxDigits=${this.maxDigits}
563
595
  .hintedZeros=${config.hintedZeros}
564
596
  .spaceReserver=${config.spaceReserver}
565
597
  .off=${config.off ?? false}
@@ -1135,6 +1167,7 @@ export class ObcReadout extends LitElement {
1135
1167
  // `changed`, skip the check, and render the invalid value as a plain dash:
1136
1168
  // exactly the silent failure this assertion exists to prevent.
1137
1169
  assertReadoutValueType('obc-readout', this.value, this.valueType);
1170
+ assertReadoutFractionDigits('obc-readout', this.fractionDigits);
1138
1171
  }
1139
1172
 
1140
1173
  override updated(changed: Map<string, unknown>): void {
@@ -4,6 +4,7 @@ import componentStyle from './readout-list.css?inline';
4
4
  import {customElement} from '../../decorator.js';
5
5
  import '../readout-list-item/readout-list-item.js';
6
6
  import {ObcReadoutListItem} from '../readout-list-item/readout-list-item.js';
7
+ import {resolveReadoutDigitCount} from '../readout/readout-formatters.js';
7
8
  import {
8
9
  resolveReadoutNumericValue,
9
10
  ReadoutValueType,
@@ -158,12 +159,19 @@ export class ObcReadoutList extends LitElement {
158
159
  // here, so they take part as usual.
159
160
  const hasNumericBlock =
160
161
  !isTextValueRow(item) || item.hasSetpoint || item.hasAdvice;
162
+ // Bounded per row: the list builds its own reserver string from these
163
+ // counts, so an `Infinity` or absurdly large value on any single row
164
+ // would otherwise throw out of `String.prototype.repeat` below — inside a
165
+ // MutationObserver callback, where the stack says nothing useful.
161
166
  if (hasNumericBlock) {
162
167
  maxFractionDigits = Math.max(
163
168
  maxFractionDigits,
164
- item.fractionDigits ?? 0
169
+ resolveReadoutDigitCount(item.fractionDigits)
170
+ );
171
+ maxIntegerDigits = Math.max(
172
+ maxIntegerDigits,
173
+ resolveReadoutDigitCount(item.maxDigits)
165
174
  );
166
- maxIntegerDigits = Math.max(maxIntegerDigits, item.maxDigits ?? 0);
167
175
  }
168
176
  maxIntegerDigits = Math.max(
169
177
  maxIntegerDigits,
@@ -17,6 +17,9 @@ import {
17
17
  } from '../../building-blocks/readout-block/readout-block.js';
18
18
  import {
19
19
  assertReadoutValueType,
20
+ assertReadoutFractionDigits,
21
+ isReadoutDigitCountMissing,
22
+ resolveReadoutDigitCount,
20
23
  resolveReadoutNumericValue,
21
24
  ReadoutValueType,
22
25
  type ReadoutNumericFormatOptions,
@@ -289,13 +292,22 @@ export class ObcReadoutListItem extends LitElement {
289
292
  @property({type: Boolean}) hasDegreeSpacer = false;
290
293
  /**
291
294
  * Also formats the numeric setpoint / advice blocks, which stay numeric
292
- * even when the value is text.
295
+ * even when the value is text. Must be between 0 and 100 — the range
296
+ * `Number.prototype.toFixed` accepts; outside it throws a `RangeError`.
297
+ * A fractional count truncates (`2.7` → `2`). A count that never arrived
298
+ * (`NaN`, `null` or unset) renders the reading as the unavailable dash —
299
+ * formatting with a precision the author never chose would let a critical
300
+ * `0.4` pass for a healthy `0`.
293
301
  * @availableWhen valueType==number || hasSetpoint==true || hasAdvice==true
294
302
  */
295
303
  @property({type: Number}) fractionDigits = 0;
296
304
  /**
297
305
  * Also formats the numeric setpoint / advice blocks, which stay numeric
298
- * even when the value is text.
306
+ * even when the value is text. Bounded before use: a fractional count
307
+ * truncates (`2.7` → `2`), anything above 100 — including `Infinity` —
308
+ * caps at 100, and a negative count reserves nothing. A count that never
309
+ * arrived (`NaN`, `null` or unset) renders the reading as the unavailable
310
+ * dash, consistent with `fractionDigits`.
299
311
  * @availableWhen valueType==number || hasSetpoint==true || hasAdvice==true
300
312
  */
301
313
  @property({type: Number}) maxDigits = 0;
@@ -343,7 +355,21 @@ export class ObcReadoutListItem extends LitElement {
343
355
  }
344
356
 
345
357
  private get resolvedMaxDigits(): number {
346
- return this.maxDigits ?? 0;
358
+ return resolveReadoutDigitCount(this.maxDigits);
359
+ }
360
+
361
+ /**
362
+ * Whether a digit knob failed to arrive (`NaN` / `null` / `undefined`).
363
+ * The raw knobs are forwarded to the blocks, which render the reading as
364
+ * the unavailable dash (see `obc-readout-block.digitCountsMissing`); this
365
+ * getter only keeps the setpoint comparison in agreement with what is
366
+ * displayed.
367
+ */
368
+ private get digitCountsMissing(): boolean {
369
+ return (
370
+ isReadoutDigitCountMissing(this.fractionDigits) ||
371
+ isReadoutDigitCountMissing(this.maxDigits)
372
+ );
347
373
  }
348
374
 
349
375
  private get resolvedClickable(): false | Required<ReadoutListItemClickable> {
@@ -361,6 +387,12 @@ export class ObcReadoutListItem extends LitElement {
361
387
  if (!this.hasSetpoint) {
362
388
  return false;
363
389
  }
390
+ // A missing digit knob renders every numeric block as the dash; two
391
+ // dashes must not read as "at the setpoint", so the comparison stays off
392
+ // entirely while the configuration is untrustworthy.
393
+ if (this.digitCountsMissing) {
394
+ return false;
395
+ }
364
396
  // A text value never compares equal to a setpoint, so flip-flop / pop-up
365
397
  // stay dormant for `valueType="text"`.
366
398
  return isDisplayedAtSetpoint(
@@ -516,8 +548,8 @@ export class ObcReadoutListItem extends LitElement {
516
548
  .weight=${config.weight}
517
549
  .hasDegree=${config.hasDegree ?? false}
518
550
  .hasIcon=${config.hasIcon ?? false}
519
- .fractionDigits=${this.resolvedFractionDigits}
520
- .maxDigits=${this.resolvedMaxDigits}
551
+ .fractionDigits=${this.fractionDigits}
552
+ .maxDigits=${this.maxDigits}
521
553
  .hintedZeros=${config.hintedZeros}
522
554
  .spaceReserver=${config.spaceReserver}
523
555
  .off=${config.off ?? false}
@@ -869,6 +901,7 @@ export class ObcReadoutListItem extends LitElement {
869
901
  // `changed`, skip the check, and render the invalid value as a plain dash:
870
902
  // exactly the silent failure this assertion exists to prevent.
871
903
  assertReadoutValueType('obc-readout-list-item', this.value, this.valueType);
904
+ assertReadoutFractionDigits('obc-readout-list-item', this.fractionDigits);
872
905
  }
873
906
 
874
907
  override updated(changed: Map<string, unknown>): void {