@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.111 → 2.0.0-next.113

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 (85) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +185 -29
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +180 -21
  4. package/dist/building-blocks/readout-block/readout-block.d.ts +29 -5
  5. package/dist/building-blocks/readout-block/readout-block.d.ts.map +1 -1
  6. package/dist/building-blocks/readout-block/readout-block.js +21 -7
  7. package/dist/building-blocks/readout-block/readout-block.js.map +1 -1
  8. package/dist/navigation-instruments/compass/compass.d.ts +1 -1
  9. package/dist/navigation-instruments/compass/compass.d.ts.map +1 -1
  10. package/dist/navigation-instruments/compass/compass.js +1 -1
  11. package/dist/navigation-instruments/compass/compass.js.map +1 -1
  12. package/dist/navigation-instruments/current/current.d.ts +25 -0
  13. package/dist/navigation-instruments/current/current.d.ts.map +1 -1
  14. package/dist/navigation-instruments/current/current.js +25 -4
  15. package/dist/navigation-instruments/current/current.js.map +1 -1
  16. package/dist/navigation-instruments/heading/heading.js +1 -1
  17. package/dist/navigation-instruments/heading/heading.js.map +1 -1
  18. package/dist/navigation-instruments/pitch-roll-heave/pitch-roll-heave.d.ts +6 -0
  19. package/dist/navigation-instruments/pitch-roll-heave/pitch-roll-heave.d.ts.map +1 -1
  20. package/dist/navigation-instruments/pitch-roll-heave/pitch-roll-heave.js +8 -1
  21. package/dist/navigation-instruments/pitch-roll-heave/pitch-roll-heave.js.map +1 -1
  22. package/dist/navigation-instruments/rate-of-turn/rate-of-turn.d.ts +5 -2
  23. package/dist/navigation-instruments/rate-of-turn/rate-of-turn.d.ts.map +1 -1
  24. package/dist/navigation-instruments/rate-of-turn/rate-of-turn.js.map +1 -1
  25. package/dist/navigation-instruments/readout/readout-formatters.d.ts +31 -0
  26. package/dist/navigation-instruments/readout/readout-formatters.d.ts.map +1 -1
  27. package/dist/navigation-instruments/readout/readout-formatters.js +55 -1
  28. package/dist/navigation-instruments/readout/readout-formatters.js.map +1 -1
  29. package/dist/navigation-instruments/readout/readout.d.ts +26 -1
  30. package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
  31. package/dist/navigation-instruments/readout/readout.js +16 -3
  32. package/dist/navigation-instruments/readout/readout.js.map +1 -1
  33. package/dist/navigation-instruments/readout-list/readout-list.d.ts +6 -1
  34. package/dist/navigation-instruments/readout-list/readout-list.d.ts.map +1 -1
  35. package/dist/navigation-instruments/readout-list/readout-list.js +21 -4
  36. package/dist/navigation-instruments/readout-list/readout-list.js.map +1 -1
  37. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts +26 -1
  38. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
  39. package/dist/navigation-instruments/readout-list-item/readout-list-item.js +15 -3
  40. package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
  41. package/dist/navigation-instruments/top-view-propulsion/top-view-propulsion.d.ts +6 -0
  42. package/dist/navigation-instruments/top-view-propulsion/top-view-propulsion.d.ts.map +1 -1
  43. package/dist/navigation-instruments/top-view-propulsion/top-view-propulsion.js +19 -4
  44. package/dist/navigation-instruments/top-view-propulsion/top-view-propulsion.js.map +1 -1
  45. package/dist/navigation-instruments/watch/environment.d.ts +8 -0
  46. package/dist/navigation-instruments/watch/environment.d.ts.map +1 -1
  47. package/dist/navigation-instruments/watch/environment.js +3 -0
  48. package/dist/navigation-instruments/watch/environment.js.map +1 -1
  49. package/dist/navigation-instruments/watch/force-pattern.d.ts +20 -2
  50. package/dist/navigation-instruments/watch/force-pattern.d.ts.map +1 -1
  51. package/dist/navigation-instruments/watch/force-pattern.js +12 -5
  52. package/dist/navigation-instruments/watch/force-pattern.js.map +1 -1
  53. package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
  54. package/dist/navigation-instruments/watch/watch.js +6 -4
  55. package/dist/navigation-instruments/watch/watch.js.map +1 -1
  56. package/dist/navigation-instruments/wind/wind.d.ts +5 -2
  57. package/dist/navigation-instruments/wind/wind.d.ts.map +1 -1
  58. package/dist/navigation-instruments/wind/wind.js +5 -1
  59. package/dist/navigation-instruments/wind/wind.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/building-blocks/readout-block/readout-block.stories.ts +112 -3
  62. package/src/building-blocks/readout-block/readout-block.ts +64 -10
  63. package/src/navigation-instruments/compass/compass.ts +2 -2
  64. package/src/navigation-instruments/current/current.stories.ts +33 -0
  65. package/src/navigation-instruments/current/current.ts +37 -3
  66. package/src/navigation-instruments/heading/heading.stories.ts +6 -4
  67. package/src/navigation-instruments/heading/heading.ts +1 -1
  68. package/src/navigation-instruments/pitch-roll-heave/pitch-roll-heave.ts +8 -1
  69. package/src/navigation-instruments/rate-of-turn/rate-of-turn.stories.ts +3 -3
  70. package/src/navigation-instruments/rate-of-turn/rate-of-turn.ts +5 -2
  71. package/src/navigation-instruments/readout/readout-formatters.spec.ts +229 -0
  72. package/src/navigation-instruments/readout/readout-formatters.ts +105 -0
  73. package/src/navigation-instruments/readout/readout.stories.ts +167 -3
  74. package/src/navigation-instruments/readout/readout.ts +52 -4
  75. package/src/navigation-instruments/readout-list/readout-list.stories.ts +122 -1
  76. package/src/navigation-instruments/readout-list/readout-list.ts +54 -5
  77. package/src/navigation-instruments/readout-list-item/readout-list-item.stories.ts +124 -4
  78. package/src/navigation-instruments/readout-list-item/readout-list-item.ts +53 -4
  79. package/src/navigation-instruments/top-view-propulsion/top-view-propulsion.ts +20 -2
  80. package/src/navigation-instruments/watch/environment.ts +10 -0
  81. package/src/navigation-instruments/watch/force-pattern.spec.ts +42 -0
  82. package/src/navigation-instruments/watch/force-pattern.ts +44 -7
  83. package/src/navigation-instruments/watch/watch.ts +20 -5
  84. package/src/navigation-instruments/wind/wind.spec.ts +3 -1
  85. package/src/navigation-instruments/wind/wind.ts +12 -3
@@ -0,0 +1,229 @@
1
+ import {describe, it, expect} from 'vitest';
2
+ import {
3
+ assertReadoutValueType,
4
+ resolveReadoutNumericValue,
5
+ resolveReadoutTextValue,
6
+ isReadoutValueType,
7
+ ReadoutValueType,
8
+ } from './readout-formatters.js';
9
+
10
+ describe('assertReadoutValueType', () => {
11
+ it('accepts a number when valueType is number', () => {
12
+ expect(() =>
13
+ assertReadoutValueType('obc-readout', 12.4, ReadoutValueType.number)
14
+ ).not.toThrow();
15
+ });
16
+
17
+ it('accepts null when valueType is number', () => {
18
+ expect(() =>
19
+ assertReadoutValueType('obc-readout', null, ReadoutValueType.number)
20
+ ).not.toThrow();
21
+ });
22
+
23
+ // HTML attributes are always strings, so `<obc-readout value="12.4">` must
24
+ // keep working under the default valueType.
25
+ it('accepts a numeric-looking string when valueType is number', () => {
26
+ expect(() =>
27
+ assertReadoutValueType('obc-readout', '12.4', ReadoutValueType.number)
28
+ ).not.toThrow();
29
+ });
30
+
31
+ it('accepts a negative numeric string when valueType is number', () => {
32
+ expect(() =>
33
+ assertReadoutValueType('obc-readout', '-12.4', ReadoutValueType.number)
34
+ ).not.toThrow();
35
+ });
36
+
37
+ // `value="${maybeUndefined}"` is a common template shape, and Number('') is
38
+ // 0 — a silently wrong reading, which is worse than a dash.
39
+ it('accepts a blank string when valueType is number', () => {
40
+ expect(() =>
41
+ assertReadoutValueType('obc-readout', '', ReadoutValueType.number)
42
+ ).not.toThrow();
43
+ expect(() =>
44
+ assertReadoutValueType('obc-readout', ' ', ReadoutValueType.number)
45
+ ).not.toThrow();
46
+ });
47
+
48
+ it('throws for text when valueType is number', () => {
49
+ expect(() =>
50
+ assertReadoutValueType('obc-readout', 'Auto', ReadoutValueType.number)
51
+ ).toThrow(TypeError);
52
+ });
53
+
54
+ it('names the offending tag and value in the message', () => {
55
+ expect(() =>
56
+ assertReadoutValueType(
57
+ 'obc-readout-list-item',
58
+ 'Thermo On',
59
+ ReadoutValueType.number
60
+ )
61
+ ).toThrow(/obc-readout-list-item.*Thermo On.*valueType="text"/s);
62
+ });
63
+
64
+ it('throws for a partially numeric string when valueType is number', () => {
65
+ expect(() =>
66
+ assertReadoutValueType('obc-readout', '12.4 kn', ReadoutValueType.number)
67
+ ).toThrow(TypeError);
68
+ });
69
+
70
+ it('never throws when valueType is text', () => {
71
+ expect(() =>
72
+ assertReadoutValueType('obc-readout', 'Auto', ReadoutValueType.text)
73
+ ).not.toThrow();
74
+ expect(() =>
75
+ assertReadoutValueType('obc-readout', 12.4, ReadoutValueType.text)
76
+ ).not.toThrow();
77
+ });
78
+
79
+ // An attribute carries an unchecked string, so a typo matches neither mode.
80
+ // Without this it falls through every check and silently renders a dash.
81
+ it('throws for an unrecognised valueType', () => {
82
+ expect(() =>
83
+ assertReadoutValueType(
84
+ 'obc-readout',
85
+ 'Auto',
86
+ 'strng' as unknown as ReadoutValueType
87
+ )
88
+ ).toThrow(/valueType must be "number" or "text".*"strng"/s);
89
+ });
90
+
91
+ it('throws for an unrecognised valueType even with a numeric value', () => {
92
+ expect(() =>
93
+ assertReadoutValueType(
94
+ 'obc-readout',
95
+ 12.4,
96
+ 'STRING' as unknown as ReadoutValueType
97
+ )
98
+ ).toThrow(TypeError);
99
+ });
100
+
101
+ // undefined/null mean "use the default", so they must still enforce the
102
+ // number contract rather than skipping validation entirely.
103
+ it('treats an unset valueType as number', () => {
104
+ expect(() =>
105
+ assertReadoutValueType(
106
+ 'obc-readout',
107
+ 'Auto',
108
+ undefined as unknown as ReadoutValueType
109
+ )
110
+ ).toThrow(TypeError);
111
+ expect(() =>
112
+ assertReadoutValueType(
113
+ 'obc-readout',
114
+ 12.4,
115
+ undefined as unknown as ReadoutValueType
116
+ )
117
+ ).not.toThrow();
118
+ });
119
+ });
120
+
121
+ describe('isReadoutValueType', () => {
122
+ it('accepts the supported values', () => {
123
+ expect(isReadoutValueType('number')).toBe(true);
124
+ expect(isReadoutValueType('text')).toBe(true);
125
+ });
126
+
127
+ it('rejects anything else', () => {
128
+ // `string` names the JS type rather than the mode, and is the likely
129
+ // mistake for anyone reaching for the TypeScript word.
130
+ expect(isReadoutValueType('string')).toBe(false);
131
+ expect(isReadoutValueType('strng')).toBe(false);
132
+ expect(isReadoutValueType('Number')).toBe(false);
133
+ expect(isReadoutValueType('')).toBe(false);
134
+ expect(isReadoutValueType(undefined)).toBe(false);
135
+ expect(isReadoutValueType(null)).toBe(false);
136
+ expect(isReadoutValueType(0)).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe('resolveReadoutNumericValue', () => {
141
+ it('passes a number through unchanged', () => {
142
+ expect(resolveReadoutNumericValue(12.4, ReadoutValueType.number)).toBe(
143
+ 12.4
144
+ );
145
+ });
146
+
147
+ it('parses a numeric string', () => {
148
+ expect(resolveReadoutNumericValue('12.4', ReadoutValueType.number)).toBe(
149
+ 12.4
150
+ );
151
+ });
152
+
153
+ it('is undefined for null, undefined and blank strings', () => {
154
+ expect(
155
+ resolveReadoutNumericValue(null, ReadoutValueType.number)
156
+ ).toBeUndefined();
157
+ expect(
158
+ resolveReadoutNumericValue(undefined, ReadoutValueType.number)
159
+ ).toBeUndefined();
160
+ expect(
161
+ resolveReadoutNumericValue('', ReadoutValueType.number)
162
+ ).toBeUndefined();
163
+ expect(
164
+ resolveReadoutNumericValue(' ', ReadoutValueType.number)
165
+ ).toBeUndefined();
166
+ });
167
+
168
+ it('is undefined for non-numeric text', () => {
169
+ expect(
170
+ resolveReadoutNumericValue('Auto', ReadoutValueType.number)
171
+ ).toBeUndefined();
172
+ });
173
+
174
+ // Text rows must contribute nothing to obc-readout-list's shared reserver.
175
+ it('is undefined in text mode even for a numeric value', () => {
176
+ expect(
177
+ resolveReadoutNumericValue(12.4, ReadoutValueType.text)
178
+ ).toBeUndefined();
179
+ expect(
180
+ resolveReadoutNumericValue('12.4', ReadoutValueType.text)
181
+ ).toBeUndefined();
182
+ });
183
+
184
+ // Pre-existing behaviour, deliberately preserved: NaN reaches the formatter.
185
+ it('passes NaN through in number mode', () => {
186
+ expect(
187
+ resolveReadoutNumericValue(Number.NaN, ReadoutValueType.number)
188
+ ).toBeNaN();
189
+ });
190
+ });
191
+
192
+ describe('resolveReadoutTextValue', () => {
193
+ it('returns the text verbatim in text mode', () => {
194
+ expect(resolveReadoutTextValue('Thermo On', ReadoutValueType.text)).toBe(
195
+ 'Thermo On'
196
+ );
197
+ });
198
+
199
+ // The reason a converter was rejected: it would have parsed this to 1.5.
200
+ it('preserves trailing zeros in text mode', () => {
201
+ expect(resolveReadoutTextValue('1.50', ReadoutValueType.text)).toBe('1.50');
202
+ });
203
+
204
+ it('coerces a number to text in text mode', () => {
205
+ expect(resolveReadoutTextValue(12.4, ReadoutValueType.text)).toBe('12.4');
206
+ });
207
+
208
+ it('is undefined for null, undefined and blank strings', () => {
209
+ expect(
210
+ resolveReadoutTextValue(null, ReadoutValueType.text)
211
+ ).toBeUndefined();
212
+ expect(
213
+ resolveReadoutTextValue(undefined, ReadoutValueType.text)
214
+ ).toBeUndefined();
215
+ expect(resolveReadoutTextValue('', ReadoutValueType.text)).toBeUndefined();
216
+ expect(
217
+ resolveReadoutTextValue(' ', ReadoutValueType.text)
218
+ ).toBeUndefined();
219
+ });
220
+
221
+ it('is undefined in number mode', () => {
222
+ expect(
223
+ resolveReadoutTextValue('12.4', ReadoutValueType.number)
224
+ ).toBeUndefined();
225
+ expect(
226
+ resolveReadoutTextValue(12.4, ReadoutValueType.number)
227
+ ).toBeUndefined();
228
+ });
229
+ });
@@ -4,6 +4,111 @@ export type ReadoutNumericFormatOptions = {
4
4
  fractionDigits: number;
5
5
  };
6
6
 
7
+ /**
8
+ * How a readout's `value` is interpreted.
9
+ * - `number`: formatted via `fractionDigits` / `maxDigits`.
10
+ * - `text`: rendered verbatim, with the numeric format options ignored.
11
+ */
12
+ export enum ReadoutValueType {
13
+ number = 'number',
14
+ text = 'text',
15
+ }
16
+
17
+ const READOUT_VALUE_TYPES: readonly string[] = Object.values(ReadoutValueType);
18
+
19
+ /** Whether `value` is one of the supported {@link ReadoutValueType} values. */
20
+ export function isReadoutValueType(value: unknown): value is ReadoutValueType {
21
+ return typeof value === 'string' && READOUT_VALUE_TYPES.includes(value);
22
+ }
23
+
24
+ function isBlank(value: string): boolean {
25
+ return value.trim() === '';
26
+ }
27
+
28
+ /**
29
+ * Throws when `valueType` is not a supported value, or when `value` is text but
30
+ * `valueType` is `number`.
31
+ *
32
+ * `valueType` is validated first because an attribute carries an unchecked
33
+ * string: a typo such as `valuetype="strng"` matches neither mode, so every
34
+ * mode check falls through and the readout silently renders the unavailable
35
+ * dash — the opposite of the loud failure this contract exists to give.
36
+ * `undefined`/`null` are allowed and mean "use the default".
37
+ *
38
+ * Attributes are always strings, so a numeric-looking string (`value="10.12"`)
39
+ * is accepted and parsed. Blank strings resolve to the unavailable dash rather
40
+ * than throwing — `value="${maybeUndefined}"` is a common template shape, and
41
+ * `Number('')` is `0`, a silently wrong reading.
42
+ */
43
+ export function assertReadoutValueType(
44
+ tagName: string,
45
+ value: number | string | null | undefined,
46
+ valueType: ReadoutValueType
47
+ ): void {
48
+ // `undefined`/`null` mean "use the default", matching how the components and
49
+ // the resolvers treat an unset `valueType`.
50
+ const resolved = valueType ?? ReadoutValueType.number;
51
+ if (!isReadoutValueType(resolved)) {
52
+ throw new TypeError(
53
+ `<${tagName}>: valueType must be ` +
54
+ `${READOUT_VALUE_TYPES.map((t) => `"${t}"`).join(' or ')} ` +
55
+ `(got ${JSON.stringify(valueType)}).`
56
+ );
57
+ }
58
+ if (
59
+ resolved !== ReadoutValueType.number ||
60
+ typeof value !== 'string' ||
61
+ isBlank(value)
62
+ ) {
63
+ return;
64
+ }
65
+ if (Number.isFinite(Number(value))) {
66
+ return;
67
+ }
68
+ throw new TypeError(
69
+ `<${tagName}>: value must be a number when valueType is "number" ` +
70
+ `(got ${JSON.stringify(value)}). Set valueType="text" to render text.`
71
+ );
72
+ }
73
+
74
+ /** The value as a number, or `undefined` when it is text / unavailable. */
75
+ export function resolveReadoutNumericValue(
76
+ value: number | string | null | undefined,
77
+ valueType: ReadoutValueType
78
+ ): number | undefined {
79
+ if (
80
+ valueType === ReadoutValueType.text ||
81
+ value === null ||
82
+ value === undefined
83
+ ) {
84
+ return undefined;
85
+ }
86
+ if (typeof value === 'number') {
87
+ return value;
88
+ }
89
+ if (isBlank(value)) {
90
+ return undefined;
91
+ }
92
+ const parsed = Number(value);
93
+ return Number.isFinite(parsed) ? parsed : undefined;
94
+ }
95
+
96
+ /** The value as display text, or `undefined` when not in text mode / blank. */
97
+ export function resolveReadoutTextValue(
98
+ value: number | string | null | undefined,
99
+ valueType: ReadoutValueType
100
+ ): string | undefined {
101
+ if (
102
+ valueType !== ReadoutValueType.text ||
103
+ value === null ||
104
+ value === undefined
105
+ ) {
106
+ return undefined;
107
+ }
108
+ const text = typeof value === 'number' ? String(value) : value;
109
+ return isBlank(text) ? undefined : text;
110
+ }
111
+
7
112
  function dashedGenerator({
8
113
  showZeroPadding,
9
114
  minValueLength,
@@ -20,6 +20,7 @@ import {
20
20
  type ReadoutAdviceOptions,
21
21
  type ReadoutReserverOptions,
22
22
  type ReadoutSourceOptions,
23
+ ReadoutValueType,
23
24
  } from './readout.js';
24
25
  import {
25
26
  type AlertFrameConfig,
@@ -36,7 +37,8 @@ type ReadoutStoryArgs = {
36
37
  unit: string;
37
38
  src: string;
38
39
  hasValue: boolean;
39
- value: number;
40
+ value: number | string;
41
+ valueType: ReadoutValueType;
40
42
  off: boolean;
41
43
  hasSetpoint: boolean;
42
44
  setpoint: number;
@@ -90,7 +92,8 @@ type ReadoutConfig = {
90
92
  unit?: string;
91
93
  src?: string;
92
94
  hasValue?: boolean;
93
- value?: number | null;
95
+ value?: number | string | null;
96
+ valueType?: ReadoutValueType;
94
97
  off?: boolean;
95
98
  hasSetpoint?: boolean;
96
99
  setpoint?: number;
@@ -159,6 +162,7 @@ function renderReadout(config: ReadoutConfig) {
159
162
  .src=${config.src}
160
163
  .hasValue=${config.hasValue ?? true}
161
164
  .value=${config.value ?? null}
165
+ .valueType=${config.valueType ?? ReadoutValueType.number}
162
166
  .off=${config.off ?? false}
163
167
  .hasSetpoint=${config.hasSetpoint ?? false}
164
168
  .setpoint=${config.setpoint}
@@ -241,6 +245,7 @@ const defaultArgs: ReadoutStoryArgs = {
241
245
  src: '',
242
246
  hasValue: true,
243
247
  value: 123,
248
+ valueType: ReadoutValueType.number,
244
249
  off: false,
245
250
  hasSetpoint: false,
246
251
  setpoint: 120,
@@ -306,6 +311,7 @@ const meta = {
306
311
  src: args.src,
307
312
  hasValue: args.hasValue,
308
313
  value: args.value,
314
+ valueType: args.valueType,
309
315
  off: args.off,
310
316
  hasSetpoint: args.hasSetpoint,
311
317
  setpoint: args.setpoint,
@@ -322,7 +328,16 @@ const meta = {
322
328
  hasValue: {name: 'Has Value', table: {category: 'Data'}},
323
329
  value: {
324
330
  name: 'Value',
325
- control: {type: 'number'},
331
+ // Text control (not number) so both value types are exercisable. Under
332
+ // valueType=number a numeric string resolves back to a number; entering
333
+ // non-numeric text there throws, which is the intended contract.
334
+ control: {type: 'text'},
335
+ table: {category: 'Data'},
336
+ },
337
+ valueType: {
338
+ name: 'Value Type',
339
+ control: {type: 'inline-radio'},
340
+ options: Object.values(ReadoutValueType),
326
341
  table: {category: 'Data'},
327
342
  },
328
343
  off: {name: 'Off', table: {category: 'Data'}},
@@ -1153,6 +1168,141 @@ export const DebugOverlay: StoryObj<
1153
1168
  `,
1154
1169
  };
1155
1170
 
1171
+ /**
1172
+ * **Text values** — `valueType="text"` renders `value` verbatim instead of
1173
+ * formatting it as a number, for readings that are states rather than
1174
+ * quantities ("Auto", "Thermo On", "Standby").
1175
+ *
1176
+ * The numeric format options (`fractionDigits`, `maxDigits`, `hintedZeros`) are
1177
+ * ignored in this mode; an explicit `spaceReserver` still applies. Passing text
1178
+ * while `valueType` is `number` throws a `TypeError` rather than rendering
1179
+ * `NaN` — a numeric-looking string such as `"12.4"` is still accepted and
1180
+ * parsed, so plain-HTML `value="12.4"` keeps working.
1181
+ */
1182
+ export const TextValue: Story = {
1183
+ render: () =>
1184
+ renderShowcase([
1185
+ {
1186
+ title: 'Text Value — Vertical',
1187
+ columns: 3,
1188
+ cases: [
1189
+ {
1190
+ label: 'text',
1191
+ config: {
1192
+ label: 'Mode',
1193
+ unit: '',
1194
+ value: 'Auto',
1195
+ valueType: ReadoutValueType.text,
1196
+ },
1197
+ },
1198
+ {
1199
+ label: 'text + source',
1200
+ config: {
1201
+ label: 'Thruster',
1202
+ unit: '',
1203
+ value: 'Standby',
1204
+ valueType: ReadoutValueType.text,
1205
+ src: 'CTRL',
1206
+ },
1207
+ },
1208
+ {
1209
+ label: 'text + stacked meta',
1210
+ config: {
1211
+ label: 'Status',
1212
+ unit: '',
1213
+ value: 'Normal',
1214
+ valueType: ReadoutValueType.text,
1215
+ options: {stacking: ReadoutStacking.stacked},
1216
+ },
1217
+ },
1218
+ ],
1219
+ },
1220
+ {
1221
+ title: 'Text Value — Horizontal',
1222
+ columns: 3,
1223
+ cases: [
1224
+ {
1225
+ label: 'text',
1226
+ config: {
1227
+ label: 'Mode',
1228
+ unit: '',
1229
+ value: 'Auto',
1230
+ valueType: ReadoutValueType.text,
1231
+ options: {direction: ReadoutDirection.horizontal},
1232
+ },
1233
+ },
1234
+ {
1235
+ label: 'text + source',
1236
+ config: {
1237
+ label: 'Thruster',
1238
+ unit: '',
1239
+ value: 'Standby',
1240
+ valueType: ReadoutValueType.text,
1241
+ src: 'CTRL',
1242
+ options: {direction: ReadoutDirection.horizontal},
1243
+ },
1244
+ },
1245
+ {
1246
+ label: 'numeric (contrast)',
1247
+ config: {
1248
+ label: 'SOG',
1249
+ unit: 'kn',
1250
+ value: 12.4,
1251
+ options: {
1252
+ direction: ReadoutDirection.horizontal,
1253
+ fractionDigits: 1,
1254
+ },
1255
+ },
1256
+ },
1257
+ ],
1258
+ },
1259
+ {
1260
+ title: 'Value Resolution — Sizes and Edge Cases',
1261
+ columns: 4,
1262
+ cases: [
1263
+ {
1264
+ label: 'verbatim "1.50" (trailing zero kept)',
1265
+ config: {
1266
+ label: 'Bearing',
1267
+ unit: '',
1268
+ value: '1.50',
1269
+ valueType: ReadoutValueType.text,
1270
+ options: {fractionDigits: 1},
1271
+ },
1272
+ },
1273
+ {
1274
+ label: 'null in text mode (dash)',
1275
+ config: {
1276
+ label: 'Mode',
1277
+ unit: '',
1278
+ value: null,
1279
+ valueType: ReadoutValueType.text,
1280
+ },
1281
+ },
1282
+ {
1283
+ label: 'numeric string in number mode → 12.4',
1284
+ config: {
1285
+ label: 'SOG',
1286
+ unit: 'kn',
1287
+ value: '12.4',
1288
+ options: {fractionDigits: 1},
1289
+ },
1290
+ },
1291
+ {
1292
+ label: 'text + degree (hasDegree honoured)',
1293
+ config: {
1294
+ label: 'Mode',
1295
+ unit: '',
1296
+ value: 'Auto',
1297
+ valueType: ReadoutValueType.text,
1298
+ options: {hasDegree: true},
1299
+ },
1300
+ },
1301
+ ],
1302
+ },
1303
+ ]),
1304
+ };
1305
+
1156
1306
  export const TestCases: Story = {
1157
1307
  render: () => {
1158
1308
  return html` <style>
@@ -1174,6 +1324,20 @@ export const TestCases: Story = {
1174
1324
  .value=${123}
1175
1325
  .unit=${'kn'}
1176
1326
  ></obc-readout>
1327
+ <obc-readout
1328
+ .size=${ReadoutSize.small}
1329
+ .direction=${ReadoutDirection.horizontal}
1330
+ .value=${'Thermo On'}
1331
+ .valueType=${ReadoutValueType.text}
1332
+ .label=${'Operating mode'}
1333
+ ></obc-readout>
1334
+ <obc-readout
1335
+ .size=${ReadoutSize.small}
1336
+ .direction=${ReadoutDirection.horizontal}
1337
+ .value=${'Normal'}
1338
+ .valueType=${ReadoutValueType.text}
1339
+ .label=${'Status'}
1340
+ ></obc-readout>
1177
1341
  </div>`;
1178
1342
  },
1179
1343
  };
@@ -25,7 +25,12 @@ import {
25
25
  type ReadoutSrcOptions,
26
26
  } from '../readout-list-item/readout-list-item.js';
27
27
  import {Priority} from '../types.js';
28
- import {type ReadoutNumericFormatOptions} from './readout-formatters.js';
28
+ import {
29
+ assertReadoutValueType,
30
+ resolveReadoutNumericValue,
31
+ ReadoutValueType,
32
+ type ReadoutNumericFormatOptions,
33
+ } from './readout-formatters.js';
29
34
  import {
30
35
  isDisplayedAtSetpoint,
31
36
  readoutNumericFormatOptions,
@@ -58,6 +63,7 @@ import '../../icons/icon-drop-down-google.js';
58
63
  // purpose: the two components are layout variants of the same primitives +
59
64
  // per-block options API and may merge in a future release.
60
65
  export {ObcTextboxFontWeight} from '../../components/textbox/textbox.js';
66
+ export {ReadoutValueType} from './readout-formatters.js';
61
67
  export type {
62
68
  ReadoutBlockState,
63
69
  ReadoutValueOptions,
@@ -202,6 +208,8 @@ export interface ReadoutSourceOptions extends ReadoutSrcOptions {
202
208
  * `flyout` interactivity via `srcOptions.interaction`.
203
209
  * - **Formatting:** shared `fractionDigits`, width reservation via
204
210
  * `maxDigits` and per-segment `hintedZeros`; a `null` value renders a dash.
211
+ * - **Text values:** `valueType="text"` renders `value` verbatim (e.g.
212
+ * `"Auto"`) instead of formatting it as a number.
205
213
  *
206
214
  * ### Usage Guidelines
207
215
  * Use for stand-alone instrument readouts and readouts embedded in
@@ -242,7 +250,18 @@ export class ObcReadout extends LitElement {
242
250
  * value block at full size, so the layout does not shift when data arrives.
243
251
  */
244
252
  @property({type: Boolean, attribute: false}) hasValue = true;
245
- @property({type: Number}) value: number | null = null;
253
+ /**
254
+ * The value; `null` renders a dash. A number by default, or text when
255
+ * {@link valueType} is `text`.
256
+ */
257
+ @property({type: String}) value: number | string | null = null;
258
+ /**
259
+ * How {@link value} is interpreted. `number` (default) formats it via
260
+ * `fractionDigits`; `text` renders it verbatim and ignores the numeric
261
+ * format options. Passing text while this is `number` throws.
262
+ */
263
+ @property({type: String}) valueType: ReadoutValueType =
264
+ ReadoutValueType.number;
246
265
  /** Render the value as `offText` (e.g. equipment powered down). Affects the value only. */
247
266
  @property({type: Boolean}) off = false;
248
267
  /** Text shown in place of the value when `off` is true. @availableWhen off==true */
@@ -267,7 +286,17 @@ export class ObcReadout extends LitElement {
267
286
  @property({type: Boolean}) hasDegree = false;
268
287
  /** @availableWhen hasDegree==false */
269
288
  @property({type: Boolean}) hasDegreeSpacer = false;
289
+ /**
290
+ * Also formats the numeric setpoint / advice blocks, which stay numeric
291
+ * even when the value is text.
292
+ * @availableWhen valueType==number || hasSetpoint==true || hasAdvice==true
293
+ */
270
294
  @property({type: Number}) fractionDigits = 0;
295
+ /**
296
+ * Also formats the numeric setpoint / advice blocks, which stay numeric
297
+ * even when the value is text.
298
+ * @availableWhen valueType==number || hasSetpoint==true || hasAdvice==true
299
+ */
271
300
  @property({type: Number}) maxDigits = 0;
272
301
  @property({type: String}) dataQuality?: ReadoutDataQuality;
273
302
  // `boolean | …` (not `false | …`): the generated Angular wrapper widens a
@@ -361,8 +390,10 @@ export class ObcReadout extends LitElement {
361
390
  if (!this.hasSetpoint) {
362
391
  return false;
363
392
  }
393
+ // A text value never compares equal to a setpoint, so flip-flop / pop-up
394
+ // stay dormant for `valueType="text"`.
364
395
  return isDisplayedAtSetpoint(
365
- this.value,
396
+ resolveReadoutNumericValue(this.value, this.valueType) ?? null,
366
397
  this.setpoint,
367
398
  this.numericFormatOptions(this.resolvedMaxDigits)
368
399
  );
@@ -489,7 +520,9 @@ export class ObcReadout extends LitElement {
489
520
 
490
521
  private renderBlock(config: {
491
522
  variant: ReadoutBlockVariant;
492
- value: number | null | undefined;
523
+ value: number | string | null | undefined;
524
+ /** Only the value block is ever text; setpoint / advice stay numeric. */
525
+ valueType?: ReadoutValueType;
493
526
  valueSize: ObcTextboxSize;
494
527
  enhanced: boolean;
495
528
  weight: ObcTextboxFontWeight;
@@ -518,6 +551,7 @@ export class ObcReadout extends LitElement {
518
551
  exportparts="block, block-content, block-text, block-icon, degree"
519
552
  .variant=${config.variant}
520
553
  .value=${config.value ?? null}
554
+ .valueType=${config.valueType ?? ReadoutValueType.number}
521
555
  .size=${this.resolvedSize}
522
556
  .valueSize=${config.valueSize}
523
557
  .enhanced=${config.enhanced}
@@ -676,6 +710,7 @@ export class ObcReadout extends LitElement {
676
710
  ${this.renderBlock({
677
711
  variant: ReadoutBlockVariant.value,
678
712
  value: this.value,
713
+ valueType: this.valueType,
679
714
  valueSize: this.valueSize,
680
715
  enhanced: this.rowEnhanced,
681
716
  weight: this.valueWeight,
@@ -1017,6 +1052,7 @@ export class ObcReadout extends LitElement {
1017
1052
  : html`${this.renderBlock({
1018
1053
  variant: ReadoutBlockVariant.value,
1019
1054
  value: this.value,
1055
+ valueType: this.valueType,
1020
1056
  valueSize: this.primarySize,
1021
1057
  enhanced: false,
1022
1058
  weight: this.valueWeight,
@@ -1089,6 +1125,18 @@ export class ObcReadout extends LitElement {
1089
1125
  `;
1090
1126
  }
1091
1127
 
1128
+ protected override willUpdate(changed: Map<string, unknown>): void {
1129
+ super.willUpdate(changed);
1130
+ // Validated on EVERY update, deliberately NOT gated on `value`/`valueType`
1131
+ // appearing in `changed`. When this assertion throws, Lit's `performUpdate`
1132
+ // catch calls `__markUpdated()`, which clears the changed-properties map. A
1133
+ // later update driven by any OTHER property — inside `obc-readout-list`,
1134
+ // `align()` writing the shared reservers — would then see no `value` in
1135
+ // `changed`, skip the check, and render the invalid value as a plain dash:
1136
+ // exactly the silent failure this assertion exists to prevent.
1137
+ assertReadoutValueType('obc-readout', this.value, this.valueType);
1138
+ }
1139
+
1092
1140
  override updated(changed: Map<string, unknown>): void {
1093
1141
  super.updated(changed);
1094
1142