@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.128 → 2.0.0-next.129

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 (38) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +12321 -11895
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +227 -3
  4. package/dist/building-blocks/readout-block/readout-block.css.js +53 -19
  5. package/dist/building-blocks/readout-block/readout-block.css.js.map +1 -1
  6. package/dist/building-blocks/readout-block/readout-block.d.ts +55 -0
  7. package/dist/building-blocks/readout-block/readout-block.d.ts.map +1 -1
  8. package/dist/building-blocks/readout-block/readout-block.js +57 -2
  9. package/dist/building-blocks/readout-block/readout-block.js.map +1 -1
  10. package/dist/navigation-instruments/readout/readout-shared.d.ts +23 -0
  11. package/dist/navigation-instruments/readout/readout-shared.d.ts.map +1 -1
  12. package/dist/navigation-instruments/readout/readout-shared.js +8 -0
  13. package/dist/navigation-instruments/readout/readout-shared.js.map +1 -1
  14. package/dist/navigation-instruments/readout/readout.css.js +138 -31
  15. package/dist/navigation-instruments/readout/readout.css.js.map +1 -1
  16. package/dist/navigation-instruments/readout/readout.d.ts +63 -3
  17. package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
  18. package/dist/navigation-instruments/readout/readout.js +127 -27
  19. package/dist/navigation-instruments/readout/readout.js.map +1 -1
  20. package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js +64 -1
  21. package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js.map +1 -1
  22. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts +77 -2
  23. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
  24. package/dist/navigation-instruments/readout-list-item/readout-list-item.js +100 -23
  25. package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/building-blocks/readout-block/readout-block.css +49 -20
  28. package/src/building-blocks/readout-block/readout-block.stories.ts +54 -0
  29. package/src/building-blocks/readout-block/readout-block.ts +101 -2
  30. package/src/navigation-instruments/instrument-field/instrument-field.stories.ts +1150 -180
  31. package/src/navigation-instruments/readout/readout-shared.spec.ts +50 -0
  32. package/src/navigation-instruments/readout/readout-shared.ts +33 -0
  33. package/src/navigation-instruments/readout/readout.css +124 -32
  34. package/src/navigation-instruments/readout/readout.stories.ts +536 -19
  35. package/src/navigation-instruments/readout/readout.ts +184 -31
  36. package/src/navigation-instruments/readout-list-item/readout-list-item.css +55 -1
  37. package/src/navigation-instruments/readout-list-item/readout-list-item.stories.ts +341 -0
  38. package/src/navigation-instruments/readout-list-item/readout-list-item.ts +169 -25
@@ -1,245 +1,1215 @@
1
1
  import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
- import {ObcInstrumentField, InstrumentFieldSize} from './instrument-field.js';
2
+ import {html, nothing, type TemplateResult} from 'lit';
3
3
  import './instrument-field.js';
4
- import {html} from 'lit';
5
- import '../../components/navigation-item/navigation-item.js';
4
+ import {InstrumentFieldSize} from './instrument-field.js';
5
+ import '../readout/readout.js';
6
+ import {
7
+ ReadoutSize,
8
+ ReadoutPriority,
9
+ ReadoutDirection,
10
+ ReadoutAlignment,
11
+ ReadoutSetpointInteraction,
12
+ } from '../readout/readout.js';
13
+ import '../readout-list-item/readout-list-item.js';
14
+ import {
15
+ ReadoutListItemSize,
16
+ ReadoutListItemPriority,
17
+ ReadoutListItemStacking,
18
+ } from '../readout-list-item/readout-list-item.js';
6
19
 
7
- const meta: Meta<typeof ObcInstrumentField> = {
8
- title: 'Instruments/Instrument Field',
20
+ /**
21
+ * Migration reference: every `<obc-instrument-field>` story rendered next to
22
+ * its closest `<obc-readout>` configuration.
23
+ *
24
+ * The deprecated `<obc-instrument-field>` is on the left, the closest
25
+ * `<obc-readout>` configuration on the right, and a verdict underneath saying
26
+ * whether the two match, differ in a named way, or cannot be reconciled today.
27
+ * Verdicts were re-evaluated against the Figma 6.1 review (#1145): several
28
+ * gaps from the original comparison (#1107) are now closed — `equal-size`
29
+ * covers the legacy equal-sized setpoint layouts, and
30
+ * `labelOptions.spaceReserver` closes the label-alignment gap.
31
+ *
32
+ * These are manual-mode stories: the args panel is deliberately not wired
33
+ * because the point is a fixed, comparable pair, not a playground. Use the
34
+ * `Instruments/Readout` entry for interactive controls.
35
+ */
36
+ const meta = {
37
+ title: 'Instruments/Instrument Field (deprecated)',
9
38
  tags: ['autodocs', '6.0', 'deprecated'],
10
39
  component: 'obc-instrument-field',
11
- args: {
12
- setpoint: 0,
13
- value: 0,
14
- },
15
- argTypes: {
16
- size: {
17
- control: 'select',
18
- options: Object.values(InstrumentFieldSize),
40
+ parameters: {
41
+ docs: {
42
+ description: {
43
+ component: `# Instrument Field → Readout
44
+
45
+ \`<obc-instrument-field>\` is deprecated in favour of \`<obc-readout>\`. Each story
46
+ below shows the legacy component next to its closest \`<obc-readout>\`
47
+ configuration and states how close the result is, so external consumers have a
48
+ recipe for migrating. Verdicts follow the Figma 6.1 review (#1145).
49
+
50
+ ## Verdicts
51
+
52
+ | Badge | Meaning |
53
+ |-------|---------|
54
+ | **Fully covered** | The readout API expresses the case. Only the shared restyle below differs. |
55
+ | **Differs** | Reproducible, but with a case-specific difference on top of the restyle. |
56
+ | **Not reachable** | Cannot be produced with \`<obc-readout>\` as it stands. |
57
+
58
+ Nothing is pixel-identical: \`<obc-readout>\` is the OB 6.1 redraw of the same
59
+ information, so **Fully covered** means "no API gap", not "same pixels".
60
+
61
+ ## Shared restyle differences
62
+
63
+ These apply to *every* story below and are not repeated in each verdict.
64
+
65
+ 1. **Typography** — \`obc-instrument-field\` sizes text by \`font-size\`; the readout
66
+ stack sizes it by cap height through \`obc-textbox\`. The tiers line up by
67
+ design, not by measurement.
68
+ 2. **Label size & weight** — per the 6.1 review, large readouts render the
69
+ label at textbox \`s\` (bigger than the legacy tag) and labels are SemiBold
70
+ only on \`enhanced\`-priority readouts. The legacy \`tag\` is always regular
71
+ weight, so the weights agree at \`priority="regular"\` and differ (SemiBold)
72
+ at \`priority="enhanced"\`. \`labelOptions.size: 'xs'\` opts back into the
73
+ dense label.
74
+ 3. **Setpoint marker** — the \`obi-input-right\` chevron replaces the legacy
75
+ inline triangle. Since 6.1 the marker's size follows the setpoint block's
76
+ rendered size (16px beside an \`s\` setpoint, 24px beside \`l\`).
77
+ 4. **Setpoint emphasis** — \`<obc-readout>\` defaults to primary-secondary (the
78
+ setpoint one tier below the value). The 6.1 \`equal-size\` interaction now
79
+ reproduces the legacy layouts that rendered both at the same size:
80
+
81
+ | Legacy layout | Legacy setpoint/value | Readout equivalent |
82
+ |---|---|---|
83
+ | vertical + \`regular\` | 16px / 16px | \`size="small"\` + \`equal-size\` |
84
+ | vertical + \`enhanced\` | 16px / 34px | \`size="large"\` (default) |
85
+ | horizontal + \`regular\` | 16px / 16px | not reachable — see *Horizontal Regular* |
86
+ | horizontal + \`enhanced\` | 34px / 34px | \`size="large"\` + \`equal-size\` |
87
+
88
+ 5. **Hinted zeros** — legacy paints them \`--border-outline-color\`; per the 6.1
89
+ review the readout uses the lighter \`--element-disabled-color\` and always
90
+ regular weight.
91
+ 6. **Vertical source** — legacy renders the source directly under the label
92
+ with no divider, and since 6.1 the vertical readout does the same (only the
93
+ horizontal direction separates the source with a vertical rule).
94
+
95
+ ## Property mapping
96
+
97
+ | \`<obc-instrument-field>\` | \`<obc-readout>\` |
98
+ |---|---|
99
+ | \`size="regular"\` | \`size="small"\` |
100
+ | \`size="enhanced"\` | \`size="large"\` |
101
+ | — | \`size="medium"\` (new intermediate tier) |
102
+ | \`tag\` | \`label\` |
103
+ | \`unit\`, \`src\`, \`value\`, \`setpoint\`, \`hasSetpoint\`, \`fractionDigits\` | same names |
104
+ | \`hasSrc\` | derived from a non-blank \`src\` |
105
+ | *(default)* | \`priority="enhanced"\` |
106
+ | \`neutralColor\` | \`priority="regular"\` |
107
+ | \`showZeroPadding\` | \`valueOptions.hintedZeros\` |
108
+ | \`maxDigits\` | \`maxDigits\` — **different semantics**, see *Max Digits With Decimals* |
109
+ | \`horizontal\` | \`direction="horizontal"\` |
110
+ | \`center\` | \`alignment="center"\` |
111
+ | \`labelOnly\` | \`hasValue={false}\` |
112
+ | *(equal-sized setpoint layouts)* | \`setpointOptions.interaction="equal-size"\` |
113
+ | \`autoHideSetpoint\` | \`setpointOptions.interaction="pop-up"\` |
114
+ | \`autoHideDeadband\` | *no equivalent* — see *Auto Hide Deadband* |
115
+ | \`--obc-instrument-field-source-width\` | \`srcOptions.spaceReserver\` |
116
+ | \`--obc-instrument-field-tag-width\` | \`labelOptions.spaceReserver\` (new in 6.1) |
117
+ | \`off-value\` slot | \`offText\` (plain string only) |
118
+
119
+ The colour mapping is exact rather than approximate:
120
+ \`--element-neutral-enhanced-color\` (readout, enhanced) and
121
+ \`--instrument-enhanced-secondary-color\` (instrument-field, default) resolve to
122
+ the same value in every theme.`,
123
+ },
19
124
  },
20
125
  },
21
- } satisfies Meta<ObcInstrumentField>;
126
+ } satisfies Meta;
22
127
 
23
128
  export default meta;
24
- type Story = StoryObj<ObcInstrumentField>;
129
+ type Story = StoryObj;
130
+
131
+ /** How completely `<obc-readout>` reproduces the legacy story. */
132
+ enum Verdict {
133
+ match = 'match',
134
+ differs = 'differs',
135
+ gap = 'gap',
136
+ }
137
+
138
+ const VERDICT_LABEL: Record<Verdict, string> = {
139
+ [Verdict.match]: 'Fully covered',
140
+ [Verdict.differs]: 'Differs',
141
+ [Verdict.gap]: 'Not reachable',
142
+ };
143
+
144
+ const comparisonStyle = `
145
+ .cmp {
146
+ border: 1px solid var(--border-divider-color);
147
+ border-radius: 8px;
148
+ overflow: hidden;
149
+ max-width: 900px;
150
+ }
151
+ .cmp-head {
152
+ display: flex; align-items: center; gap: 10px;
153
+ padding: 10px 12px;
154
+ border-bottom: 1px solid var(--border-divider-color);
155
+ }
156
+ .cmp-name {
157
+ font: 600 12px/1.2 var(--global-typography-font-family, inherit);
158
+ letter-spacing: 0.06em; text-transform: uppercase;
159
+ color: var(--element-active-color);
160
+ }
161
+ .cmp-badge {
162
+ font: 600 10px/1 var(--global-typography-font-family, inherit);
163
+ letter-spacing: 0.06em; text-transform: uppercase;
164
+ padding: 4px 8px; border-radius: 10px;
165
+ border: 1px solid currentColor;
166
+ }
167
+ .cmp-badge.match { color: var(--alert-success-color); }
168
+ .cmp-badge.differs { color: var(--alert-caution-color); }
169
+ .cmp-badge.gap { color: var(--alert-alarm-color); }
170
+ .cmp-cols { display: grid; grid-template-columns: 1fr 1fr; }
171
+ .cmp-col { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
172
+ .cmp-col + .cmp-col { border-left: 1px solid var(--border-divider-color); }
173
+ .cmp-tag {
174
+ font: 10px/1.2 ui-monospace, monospace;
175
+ color: var(--element-neutral-color);
176
+ }
177
+ .cmp-stage {
178
+ display: flex; align-items: center; justify-content: center;
179
+ min-height: 92px; padding: 8px;
180
+ border-radius: 6px;
181
+ background: var(--container-background-color);
182
+ }
183
+ .cmp-alt { margin-top: 4px; }
184
+ .cmp-notes {
185
+ padding: 12px;
186
+ border-top: 1px solid var(--border-divider-color);
187
+ font: 12px/1.6 var(--global-typography-font-family, inherit);
188
+ color: var(--element-neutral-color);
189
+ }
190
+ .cmp-notes p { margin: 0 0 8px; }
191
+ .cmp-notes p:last-child { margin-bottom: 0; }
192
+ .cmp-notes code {
193
+ font: 11px/1.4 ui-monospace, monospace;
194
+ color: var(--element-active-color);
195
+ }
196
+ `;
197
+
198
+ function renderComparison(config: {
199
+ name: string;
200
+ verdict: Verdict;
201
+ legacy: TemplateResult;
202
+ modern: TemplateResult;
203
+ /** A second reference render when another configuration is the real answer. */
204
+ modernAlt?: {tag: string; content: TemplateResult};
205
+ notes: TemplateResult;
206
+ }): TemplateResult {
207
+ return html`
208
+ <style>
209
+ ${comparisonStyle}
210
+ </style>
211
+ <div class="cmp">
212
+ <div class="cmp-head">
213
+ <span class="cmp-name">${config.name}</span>
214
+ <span class="cmp-badge ${config.verdict}"
215
+ >${VERDICT_LABEL[config.verdict]}</span
216
+ >
217
+ </div>
218
+ <div class="cmp-cols">
219
+ <div class="cmp-col">
220
+ <span class="cmp-tag">&lt;obc-instrument-field&gt; (deprecated)</span>
221
+ <div class="cmp-stage">${config.legacy}</div>
222
+ </div>
223
+ <div class="cmp-col">
224
+ <span class="cmp-tag">&lt;obc-readout&gt;</span>
225
+ <div class="cmp-stage">${config.modern}</div>
226
+ ${config.modernAlt
227
+ ? html`
228
+ <span class="cmp-tag cmp-alt">${config.modernAlt.tag}</span>
229
+ <div class="cmp-stage">${config.modernAlt.content}</div>
230
+ `
231
+ : nothing}
232
+ </div>
233
+ </div>
234
+ <div class="cmp-notes">${config.notes}</div>
235
+ </div>
236
+ `;
237
+ }
238
+
239
+ // The original stories inherited `setpoint: 0` and `value: 0` from the old
240
+ // file-level `args`. Both sides below bind the *effective* values explicitly
241
+ // so each pair reproduces what that story actually rendered.
25
242
 
26
243
  export const Primary: Story = {
27
- args: {
28
- setpoint: 123,
29
- hasSetpoint: true,
30
- value: 123,
31
- tag: 'HDG',
32
- unit: 'DEG',
33
- },
244
+ render: () =>
245
+ renderComparison({
246
+ name: 'Primary',
247
+ verdict: Verdict.match,
248
+ legacy: html`
249
+ <obc-instrument-field
250
+ hasSetpoint
251
+ .setpoint=${123}
252
+ .value=${123}
253
+ tag="HDG"
254
+ unit="DEG"
255
+ ></obc-instrument-field>
256
+ `,
257
+ modern: html`
258
+ <obc-readout
259
+ .size=${ReadoutSize.small}
260
+ .priority=${ReadoutPriority.enhanced}
261
+ hasSetpoint
262
+ .setpoint=${123}
263
+ .value=${123}
264
+ .setpointOptions=${{
265
+ interaction: ReadoutSetpointInteraction.equalSize,
266
+ }}
267
+ label="HDG"
268
+ unit="DEG"
269
+ ></obc-readout>
270
+ `,
271
+ notes: html`
272
+ <p>
273
+ Same structure — setpoint over value, then label and unit on one row.
274
+ <code>size="regular"</code> maps to <code>size="small"</code>, and the
275
+ legacy default (<code>neutralColor</code> unset) maps to
276
+ <code>priority="enhanced"</code>.
277
+ </p>
278
+ <p>
279
+ The legacy <code>regular</code> layout renders setpoint and value at
280
+ the same 16px, and since 6.1
281
+ <code>setpointOptions.interaction="equal-size"</code> reproduces that
282
+ exactly (used here). Omit it and the readout defaults to
283
+ primary-secondary — the setpoint one tier below the value.
284
+ </p>
285
+ `,
286
+ }),
34
287
  };
35
288
 
36
289
  export const UndefinedSetpoint: Story = {
37
- args: {
38
- setpoint: undefined,
39
- hasSetpoint: true,
40
- value: undefined,
41
- tag: 'HDG',
42
- unit: 'DEG',
43
- },
290
+ render: () =>
291
+ renderComparison({
292
+ name: 'Undefined Setpoint',
293
+ verdict: Verdict.match,
294
+ legacy: html`
295
+ <obc-instrument-field
296
+ hasSetpoint
297
+ .setpoint=${undefined}
298
+ .value=${undefined}
299
+ tag="HDG"
300
+ unit="DEG"
301
+ ></obc-instrument-field>
302
+ `,
303
+ modern: html`
304
+ <obc-readout
305
+ .size=${ReadoutSize.small}
306
+ .priority=${ReadoutPriority.enhanced}
307
+ hasSetpoint
308
+ .setpoint=${undefined}
309
+ .value=${null}
310
+ .setpointOptions=${{
311
+ interaction: ReadoutSetpointInteraction.equalSize,
312
+ }}
313
+ label="HDG"
314
+ unit="DEG"
315
+ ></obc-readout>
316
+ `,
317
+ notes: html`
318
+ <p>
319
+ Both render the unavailable dash for value and setpoint. Note the
320
+ spelling — the two properties differ: a missing
321
+ <code>value</code> is <code>null</code> (its type is
322
+ <code>number | string | null</code>), while a missing
323
+ <code>setpoint</code> stays <code>undefined</code> (declared
324
+ <code>setpoint?: number</code>; it does not accept <code>null</code>).
325
+ </p>
326
+ <p>
327
+ Both emit a single dash here because the legacy
328
+ <code>maxDigits</code> default is 1 with padding off. The glyphs
329
+ differ deliberately: legacy renders the ASCII hyphen, while
330
+ <code>obc-readout</code> renders the digit-width U+2012 figure dash
331
+ (#1105), so its placeholder aligns with the digits it stands in for.
332
+ With padding on they also diverge in count: legacy widens the run to
333
+ <code>maxDigits</code> (<code>---</code>) while
334
+ <code>obc-readout</code> always renders one dash —
335
+ <code>readoutNumericFormatOptions()</code> hardcodes
336
+ <code>showZeroPadding: false</code>. The reserved width is unchanged
337
+ either way, so nothing shifts. The contracts live in
338
+ <code>docs/agents/readout-components.md</code>.
339
+ </p>
340
+ `,
341
+ }),
44
342
  };
45
343
 
46
344
  export const Enhanced: Story = {
47
- args: {
48
- size: InstrumentFieldSize.enhanced,
49
- hasSetpoint: true,
50
- value: 10,
51
- tag: 'HDG',
52
- unit: '/min',
53
- },
345
+ render: () =>
346
+ renderComparison({
347
+ name: 'Enhanced',
348
+ verdict: Verdict.match,
349
+ legacy: html`
350
+ <obc-instrument-field
351
+ .size=${InstrumentFieldSize.enhanced}
352
+ hasSetpoint
353
+ .setpoint=${0}
354
+ .value=${10}
355
+ tag="HDG"
356
+ unit="/min"
357
+ ></obc-instrument-field>
358
+ `,
359
+ modern: html`
360
+ <obc-readout
361
+ .size=${ReadoutSize.large}
362
+ .priority=${ReadoutPriority.enhanced}
363
+ hasSetpoint
364
+ .setpoint=${0}
365
+ .value=${10}
366
+ label="HDG"
367
+ unit="/min"
368
+ ></obc-readout>
369
+ `,
370
+ notes: html`
371
+ <p>
372
+ <code>size="enhanced"</code> maps to <code>size="large"</code>, and
373
+ this is the one legacy layout whose sizing agrees with the readout
374
+ default: a 16px setpoint above a 34px value is exactly
375
+ primary-secondary, so no <code>equal-size</code> is needed.
376
+ </p>
377
+ <p>
378
+ Since 6.1 the large readout also renders its label at textbox
379
+ <code>s</code> — visibly closer to the legacy enhanced tag than the
380
+ old <code>xs</code> label was. The setpoint reads
381
+ <code>0</code> rather than a dash — the original story inherited
382
+ <code>setpoint: 0</code> from the file-level args.
383
+ </p>
384
+ `,
385
+ }),
54
386
  };
55
387
 
56
388
  export const EnhancedWithSrc: Story = {
57
- args: {
58
- size: InstrumentFieldSize.enhanced,
59
- hasSetpoint: true,
60
- value: 10,
61
- tag: 'HDG',
62
- unit: '/min',
63
- hasSrc: true,
64
- src: 'GPS',
65
- maxDigits: 3,
66
- },
389
+ render: () =>
390
+ renderComparison({
391
+ name: 'Enhanced With Src',
392
+ verdict: Verdict.match,
393
+ legacy: html`
394
+ <obc-instrument-field
395
+ .size=${InstrumentFieldSize.enhanced}
396
+ hasSetpoint
397
+ .setpoint=${0}
398
+ .value=${10}
399
+ tag="HDG"
400
+ unit="/min"
401
+ hasSrc
402
+ src="GPS"
403
+ .maxDigits=${3}
404
+ ></obc-instrument-field>
405
+ `,
406
+ modern: html`
407
+ <obc-readout
408
+ .size=${ReadoutSize.large}
409
+ .priority=${ReadoutPriority.enhanced}
410
+ hasSetpoint
411
+ .setpoint=${0}
412
+ .value=${10}
413
+ label="HDG"
414
+ unit="/min"
415
+ src="GPS"
416
+ .maxDigits=${3}
417
+ ></obc-readout>
418
+ `,
419
+ notes: html`
420
+ <p>
421
+ <code>maxDigits: 3</code> reserves three digit widths on both sides.
422
+ Legacy renders the unfilled positions as transparent hint zeros;
423
+ <code>obc-readout-block</code> reserves them through
424
+ <code>obc-textbox</code>'s <code>length</code> slot instead. Same
425
+ result, no layout shift.
426
+ </p>
427
+ <p>
428
+ There is no <code>hasSrc</code> boolean —
429
+ <code>obc-readout</code> derives it from a non-blank <code>src</code>.
430
+ Since 6.1 the vertical readout renders the source without a divider,
431
+ exactly like the legacy layout (the original comparison still drew
432
+ one).
433
+ </p>
434
+ `,
435
+ }),
67
436
  };
68
437
 
69
438
  export const EnhancedZeroPadding: Story = {
70
- args: {
71
- size: InstrumentFieldSize.enhanced,
72
- hasSetpoint: true,
73
- value: 10,
74
- tag: 'HDG',
75
- unit: '/min',
76
- hasSrc: true,
77
- src: 'GPS',
78
- showZeroPadding: true,
79
- maxDigits: 3,
80
- },
439
+ render: () =>
440
+ renderComparison({
441
+ name: 'Enhanced Zero Padding',
442
+ verdict: Verdict.differs,
443
+ legacy: html`
444
+ <obc-instrument-field
445
+ .size=${InstrumentFieldSize.enhanced}
446
+ hasSetpoint
447
+ .setpoint=${0}
448
+ .value=${10}
449
+ tag="HDG"
450
+ unit="/min"
451
+ hasSrc
452
+ src="GPS"
453
+ showZeroPadding
454
+ .maxDigits=${3}
455
+ ></obc-instrument-field>
456
+ `,
457
+ modern: html`
458
+ <obc-readout
459
+ .size=${ReadoutSize.large}
460
+ .priority=${ReadoutPriority.enhanced}
461
+ hasSetpoint
462
+ .setpoint=${0}
463
+ .value=${10}
464
+ label="HDG"
465
+ unit="/min"
466
+ src="GPS"
467
+ .maxDigits=${3}
468
+ .valueOptions=${{hintedZeros: true}}
469
+ ></obc-readout>
470
+ `,
471
+ notes: html`
472
+ <p>
473
+ <code>showZeroPadding</code> maps to
474
+ <code>valueOptions.hintedZeros</code>, and both render
475
+ <code>010</code>. The hint styling is the 6.1-reviewed one: the
476
+ readout paints hints <code>--element-disabled-color</code> at regular
477
+ weight (legacy uses the darker <code>--border-outline-color</code>).
478
+ </p>
479
+ <p>
480
+ More importantly,
481
+ <strong><code>maxDigits</code> does not mean the same thing</strong>.
482
+ Legacy subtracts the whole formatted string — decimal point and
483
+ fraction digits included — from <code>maxDigits</code>, while
484
+ <code>obc-readout-block</code> counts integer digits only. The two
485
+ agree here because <code>fractionDigits</code> is 0; see
486
+ <em>Max Digits With Decimals</em> below for the divergence and the
487
+ translation rule.
488
+ </p>
489
+ `,
490
+ }),
81
491
  };
82
492
 
83
493
  export const NoSetpoint: Story = {
84
- args: {
85
- hasSetpoint: false,
86
- value: 10,
87
- tag: 'HDG',
88
- unit: '/min',
89
- },
494
+ render: () =>
495
+ renderComparison({
496
+ name: 'No Setpoint',
497
+ verdict: Verdict.match,
498
+ legacy: html`
499
+ <obc-instrument-field
500
+ .value=${10}
501
+ tag="HDG"
502
+ unit="/min"
503
+ ></obc-instrument-field>
504
+ `,
505
+ modern: html`
506
+ <obc-readout
507
+ .size=${ReadoutSize.small}
508
+ .priority=${ReadoutPriority.enhanced}
509
+ .value=${10}
510
+ label="HDG"
511
+ unit="/min"
512
+ ></obc-readout>
513
+ `,
514
+ notes: html`
515
+ <p>
516
+ Dropping <code>hasSetpoint</code> removes the setpoint block on both
517
+ sides; nothing else changes.
518
+ </p>
519
+ `,
520
+ }),
90
521
  };
522
+
91
523
  export const WithDecimals: Story = {
92
- args: {
93
- hasSetpoint: false,
94
- value: 1.3,
95
- fractionDigits: 2,
96
- tag: 'Offset',
97
- unit: 'm',
98
- },
524
+ render: () =>
525
+ renderComparison({
526
+ name: 'With Decimals',
527
+ verdict: Verdict.match,
528
+ legacy: html`
529
+ <obc-instrument-field
530
+ .value=${1.3}
531
+ .fractionDigits=${2}
532
+ tag="Offset"
533
+ unit="m"
534
+ ></obc-instrument-field>
535
+ `,
536
+ modern: html`
537
+ <obc-readout
538
+ .size=${ReadoutSize.small}
539
+ .priority=${ReadoutPriority.enhanced}
540
+ .value=${1.3}
541
+ .fractionDigits=${2}
542
+ label="Offset"
543
+ unit="m"
544
+ ></obc-readout>
545
+ `,
546
+ notes: html`
547
+ <p>
548
+ <code>fractionDigits: 2</code> formats identically
549
+ (<code>1.30</code>). Neither side pads: the legacy default
550
+ <code>maxDigits</code> of 1 is already shorter than the four-character
551
+ result, and <code>obc-readout</code> defaults
552
+ <code>maxDigits</code> to 0.
553
+ </p>
554
+ `,
555
+ }),
99
556
  };
100
557
 
101
558
  export const NeutralColor: Story = {
102
- args: {
103
- neutralColor: true,
104
- size: InstrumentFieldSize.enhanced,
105
- hasSetpoint: false,
106
- value: 63,
107
- tag: 'Speed',
108
- unit: 'KN',
109
- },
559
+ render: () =>
560
+ renderComparison({
561
+ name: 'Neutral Color',
562
+ verdict: Verdict.differs,
563
+ legacy: html`
564
+ <obc-instrument-field
565
+ neutralColor
566
+ .size=${InstrumentFieldSize.enhanced}
567
+ .value=${63}
568
+ tag="Speed"
569
+ unit="KN"
570
+ ></obc-instrument-field>
571
+ `,
572
+ modern: html`
573
+ <obc-readout
574
+ .size=${ReadoutSize.large}
575
+ .priority=${ReadoutPriority.regular}
576
+ .value=${63}
577
+ label="Speed"
578
+ unit="KN"
579
+ ></obc-readout>
580
+ `,
581
+ notes: html`
582
+ <p>
583
+ <code>neutralColor</code> maps to <code>priority="regular"</code> and
584
+ the value colour matches exactly
585
+ (<code>--element-neutral-color</code>).
586
+ </p>
587
+ <p>
588
+ The difference is invisible here because this story has no setpoint.
589
+ When one is present the two disagree: legacy hardcodes the setpoint to
590
+ <code>--instrument-enhanced-secondary-color</code>, so
591
+ <code>neutralColor</code> yields a grey value beside a blue setpoint.
592
+ <code>obc-readout</code> refuses that combination by design — value
593
+ and setpoint are always both neutral or both enhanced. That is a
594
+ deliberate design correction, not an oversight, but it is a visual
595
+ change for anyone relying on it.
596
+ </p>
597
+ `,
598
+ }),
110
599
  };
111
600
 
112
601
  export const Horizontal: Story = {
113
- args: {
114
- horizontal: true,
115
- size: InstrumentFieldSize.enhanced,
116
- hasSetpoint: true,
117
- setpoint: 123,
118
- value: 63,
119
- tag: 'Speed',
120
- unit: 'KN',
121
- },
602
+ render: () =>
603
+ renderComparison({
604
+ name: 'Horizontal',
605
+ verdict: Verdict.match,
606
+ legacy: html`
607
+ <obc-instrument-field
608
+ horizontal
609
+ .size=${InstrumentFieldSize.enhanced}
610
+ hasSetpoint
611
+ .setpoint=${123}
612
+ .value=${63}
613
+ tag="Speed"
614
+ unit="KN"
615
+ ></obc-instrument-field>
616
+ `,
617
+ modern: html`
618
+ <obc-readout
619
+ .direction=${ReadoutDirection.horizontal}
620
+ .size=${ReadoutSize.large}
621
+ .priority=${ReadoutPriority.enhanced}
622
+ hasSetpoint
623
+ .setpoint=${123}
624
+ .value=${63}
625
+ .setpointOptions=${{
626
+ interaction: ReadoutSetpointInteraction.equalSize,
627
+ }}
628
+ label="Speed"
629
+ unit="KN"
630
+ ></obc-readout>
631
+ `,
632
+ notes: html`
633
+ <p>
634
+ <code>horizontal</code> maps to <code>direction="horizontal"</code>,
635
+ and both stack the label over the unit beside the value.
636
+ <code>horizontal</code> + <code>enhanced</code> is the legacy layout
637
+ that renders the setpoint at the full 34px value size, and since 6.1
638
+ <code>setpointOptions.interaction="equal-size"</code> reproduces it —
639
+ <code>123</code> and <code>63</code> read as equals on both sides.
640
+ This closed the biggest sizing gap of the original comparison.
641
+ </p>
642
+ <p>
643
+ <code>obc-readout</code> draws no divider between the setpoint and the
644
+ value where the legacy component has one. The 6.1 review settled this
645
+ deliberately: "setpoint dividers are removed" — the only divider a
646
+ horizontal readout emits is the one before the source. A design
647
+ decision, not an API gap.
648
+ </p>
649
+ `,
650
+ }),
122
651
  };
123
652
 
124
653
  export const HorizontalWithSrc: Story = {
125
- args: {
126
- horizontal: true,
127
- size: InstrumentFieldSize.enhanced,
128
- hasSetpoint: true,
129
- hasSrc: true,
130
- setpoint: 123,
131
- value: 63,
132
- tag: 'Speed',
133
- unit: 'KN',
134
- src: 'GPS',
135
- },
654
+ render: () =>
655
+ renderComparison({
656
+ name: 'Horizontal With Src',
657
+ verdict: Verdict.match,
658
+ legacy: html`
659
+ <obc-instrument-field
660
+ horizontal
661
+ .size=${InstrumentFieldSize.enhanced}
662
+ hasSetpoint
663
+ hasSrc
664
+ .setpoint=${123}
665
+ .value=${63}
666
+ tag="Speed"
667
+ unit="KN"
668
+ src="GPS"
669
+ ></obc-instrument-field>
670
+ `,
671
+ modern: html`
672
+ <obc-readout
673
+ .direction=${ReadoutDirection.horizontal}
674
+ .size=${ReadoutSize.large}
675
+ .priority=${ReadoutPriority.enhanced}
676
+ hasSetpoint
677
+ .setpoint=${123}
678
+ .value=${63}
679
+ .setpointOptions=${{
680
+ interaction: ReadoutSetpointInteraction.equalSize,
681
+ }}
682
+ label="Speed"
683
+ unit="KN"
684
+ src="GPS"
685
+ ></obc-readout>
686
+ `,
687
+ notes: html`
688
+ <p>
689
+ Adding the source brings a divider on both sides —
690
+ <code>obc-readout</code> draws one automatically before the source
691
+ segment ("Source-divider are there", 6.1 review), so the legacy
692
+ <code>src-divider</code> needs no counterpart.
693
+ </p>
694
+ <p>
695
+ As in <em>Horizontal</em>: <code>equal-size</code> restores the legacy
696
+ sizing, and the missing setpoint-to-value divider is the 6.1 design.
697
+ </p>
698
+ `,
699
+ }),
136
700
  };
137
701
 
138
702
  export const VerticalCenter: Story = {
139
- args: {
140
- size: InstrumentFieldSize.enhanced,
141
- center: true,
142
- value: 1,
143
- maxDigits: 3,
144
- showZeroPadding: true,
145
- neutralColor: true,
146
- tag: 'Speed',
147
- unit: 'kn',
148
- },
703
+ render: () =>
704
+ renderComparison({
705
+ name: 'Vertical Center',
706
+ verdict: Verdict.differs,
707
+ legacy: html`
708
+ <obc-instrument-field
709
+ .size=${InstrumentFieldSize.enhanced}
710
+ center
711
+ .value=${1}
712
+ .maxDigits=${3}
713
+ showZeroPadding
714
+ neutralColor
715
+ tag="Speed"
716
+ unit="kn"
717
+ ></obc-instrument-field>
718
+ `,
719
+ modern: html`
720
+ <obc-readout
721
+ .size=${ReadoutSize.large}
722
+ .priority=${ReadoutPriority.regular}
723
+ .alignment=${ReadoutAlignment.center}
724
+ .value=${1}
725
+ .maxDigits=${3}
726
+ .valueOptions=${{hintedZeros: true}}
727
+ label="Speed"
728
+ unit="kn"
729
+ ></obc-readout>
730
+ `,
731
+ notes: html`
732
+ <p>
733
+ <code>center</code> maps to <code>alignment="center"</code>, but the
734
+ two centre different things. Legacy <code>center</code> applies
735
+ <code>justify-content: center</code> to the label row only and leaves
736
+ the value right-aligned; <code>alignment="center"</code> centres the
737
+ value cluster, the meta zone and the source together. The readout
738
+ result is the more consistent of the two.
739
+ </p>
740
+ <p>
741
+ Both render identically <em>here</em> because each component is
742
+ <code>fit-content</code> and the widest child is the value, so there
743
+ is no slack to centre within. The difference only appears once the
744
+ component is given more width than its content needs.
745
+ </p>
746
+ <p>
747
+ <code>alignment</code> is documented
748
+ <code>@availableWhen direction==vertical && stacking==stacked</code>,
749
+ yet the CSS applies it to any vertical readout — including the
750
+ <code>inline</code> stacking used here. The annotation is narrower
751
+ than the behaviour, so props tooling may hide a control that works.
752
+ </p>
753
+ `,
754
+ }),
149
755
  };
150
756
 
151
757
  export const HorizontalRegular: Story = {
152
- args: {
153
- horizontal: true,
154
- size: InstrumentFieldSize.regular,
155
- hasSetpoint: true,
156
- hasSrc: true,
157
- setpoint: 123,
158
- value: 63,
159
- tag: 'Speed',
160
- unit: 'KN',
161
- src: 'GPS',
162
- },
758
+ render: () =>
759
+ renderComparison({
760
+ name: 'Horizontal Regular',
761
+ verdict: Verdict.gap,
762
+ legacy: html`
763
+ <obc-instrument-field
764
+ horizontal
765
+ .size=${InstrumentFieldSize.regular}
766
+ hasSetpoint
767
+ hasSrc
768
+ .setpoint=${123}
769
+ .value=${63}
770
+ tag="Speed"
771
+ unit="KN"
772
+ src="GPS"
773
+ ></obc-instrument-field>
774
+ `,
775
+ modern: html`
776
+ <obc-readout
777
+ .direction=${ReadoutDirection.horizontal}
778
+ .priority=${ReadoutPriority.enhanced}
779
+ hasSetpoint
780
+ .setpoint=${123}
781
+ .value=${63}
782
+ label="Speed"
783
+ unit="KN"
784
+ src="GPS"
785
+ ></obc-readout>
786
+ `,
787
+ modernAlt: {
788
+ tag: '<obc-readout-list-item> (label-first alternative)',
789
+ content: html`
790
+ <obc-readout-list-item
791
+ .size=${ReadoutListItemSize.small}
792
+ .priority=${ReadoutListItemPriority.enhanced}
793
+ .stacking=${ReadoutListItemStacking.leadingUnit}
794
+ hasSetpoint
795
+ .setpoint=${123}
796
+ .value=${63}
797
+ label="Speed"
798
+ unit="KN"
799
+ src="GPS"
800
+ ></obc-readout-list-item>
801
+ `,
802
+ },
803
+ notes: html`
804
+ <p>
805
+ This is the one arrangement <code>obc-readout</code> cannot produce.
806
+ At <code>horizontal</code> + <code>regular</code> the legacy component
807
+ moves the tag to the <strong>front</strong> of the row (tag │ setpoint
808
+ │ value │ unit │ src). <code>obc-readout</code> always emits the meta
809
+ zone after the value, in both directions.
810
+ </p>
811
+ <p>
812
+ The 6.1 review confirmed this stays a non-goal: in the new structure
813
+ the horizontal readout carries the large value size only, and the
814
+ label-first shape is <code>obc-readout-list-item</code>'s job —
815
+ <code>stacking="leading-unit"</code>, shown underneath. It is a
816
+ different component with a row-oriented API (label left, value right),
817
+ so it is a re-layout rather than a drop-in swap.
818
+ </p>
819
+ `,
820
+ }),
163
821
  };
164
822
 
165
823
  export const LabelOnly: Story = {
166
- args: {
167
- labelOnly: true,
168
- horizontal: true,
169
- size: InstrumentFieldSize.enhanced,
170
- tag: 'Speed',
171
- unit: 'KN',
172
- },
824
+ render: () =>
825
+ renderComparison({
826
+ name: 'Label Only',
827
+ verdict: Verdict.match,
828
+ legacy: html`
829
+ <obc-instrument-field
830
+ labelOnly
831
+ horizontal
832
+ .size=${InstrumentFieldSize.enhanced}
833
+ tag="Speed"
834
+ unit="KN"
835
+ ></obc-instrument-field>
836
+ `,
837
+ modern: html`
838
+ <obc-readout
839
+ .hasValue=${false}
840
+ .direction=${ReadoutDirection.horizontal}
841
+ .size=${ReadoutSize.large}
842
+ label="Speed"
843
+ unit="KN"
844
+ ></obc-readout>
845
+ `,
846
+ notes: html`
847
+ <p>
848
+ <code>labelOnly</code> maps to <code>hasValue={false}</code>. Both
849
+ drop the value and keep the label stacked over the unit. It must be
850
+ set as a property (<code>.hasValue=\${false}</code>): the field is
851
+ declared <code>attribute: false</code> because its default is
852
+ <code>true</code>.
853
+ </p>
854
+ <p>
855
+ For a value that is merely missing right now, keep
856
+ <code>hasValue</code> and pass <code>value={null}</code> instead — the
857
+ dash holds the block at full size, so the layout does not jump when
858
+ data arrives.
859
+ </p>
860
+ `,
861
+ }),
173
862
  };
174
863
 
175
864
  export const AutoHideSetpoint: Story = {
176
- args: {
177
- autoHideSetpoint: true,
178
- autoHideDeadband: 1,
179
- size: InstrumentFieldSize.enhanced,
180
- hasSetpoint: true,
181
- value: 10,
182
- setpoint: 10,
183
- tag: 'HDG',
184
- unit: '/min',
185
- hasSrc: true,
186
- src: 'GPS',
187
- },
865
+ render: () =>
866
+ renderComparison({
867
+ name: 'Auto Hide Setpoint',
868
+ verdict: Verdict.differs,
869
+ legacy: html`
870
+ <obc-instrument-field
871
+ autoHideSetpoint
872
+ .autoHideDeadband=${1}
873
+ .size=${InstrumentFieldSize.enhanced}
874
+ hasSetpoint
875
+ .value=${10}
876
+ .setpoint=${10}
877
+ tag="HDG"
878
+ unit="/min"
879
+ hasSrc
880
+ src="GPS"
881
+ ></obc-instrument-field>
882
+ `,
883
+ modern: html`
884
+ <obc-readout
885
+ .size=${ReadoutSize.large}
886
+ .priority=${ReadoutPriority.enhanced}
887
+ hasSetpoint
888
+ .value=${10}
889
+ .setpoint=${10}
890
+ .setpointOptions=${{
891
+ interaction: ReadoutSetpointInteraction.popUp,
892
+ }}
893
+ label="HDG"
894
+ unit="/min"
895
+ src="GPS"
896
+ ></obc-readout>
897
+ `,
898
+ notes: html`
899
+ <p>
900
+ <code>autoHideSetpoint</code> maps to
901
+ <code>setpointOptions.interaction="pop-up"</code>. Both hide the
902
+ setpoint here and both keep its space, so nothing shifts. The 6.1
903
+ review confirmed this behaviour (value stays, setpoint fades); if the
904
+ collapsed reading should still carry the in-control arrow, slot an
905
+ <code>obi-input-right</code> into <code>value-icon</code> — see the
906
+ Readout <em>SetpointPopUpWithValueArrow</em> story.
907
+ </p>
908
+ <p>
909
+ The trigger differs. <code>autoHideDeadband</code> is a tolerance in
910
+ the value's own units, while pop-up compares the
911
+ <strong>rendered</strong> strings via
912
+ <code>isDisplayedAtSetpoint</code>. A deadband is therefore only
913
+ expressible through <code>fractionDigits</code> — at
914
+ <code>fractionDigits: 0</code> the readout hides within ±0.5. See
915
+ <em>Auto Hide Deadband</em> below for the case where they disagree.
916
+ </p>
917
+ `,
918
+ }),
188
919
  };
189
920
 
190
921
  export const Off: Story = {
191
- args: {
192
- off: true,
193
- size: InstrumentFieldSize.enhanced,
194
- },
922
+ render: () =>
923
+ renderComparison({
924
+ name: 'Off',
925
+ verdict: Verdict.gap,
926
+ legacy: html`
927
+ <obc-instrument-field
928
+ off
929
+ .size=${InstrumentFieldSize.enhanced}
930
+ ></obc-instrument-field>
931
+ `,
932
+ modern: html`
933
+ <obc-readout
934
+ off
935
+ .size=${ReadoutSize.large}
936
+ .priority=${ReadoutPriority.enhanced}
937
+ ></obc-readout>
938
+ `,
939
+ notes: html`
940
+ <p>
941
+ Both substitute the value with <code>OFF</code>, and the readout adds
942
+ <code>offText</code> to change the word. But the legacy
943
+ <code>off</code> state also dims the value, label and source to
944
+ <code>--element-inactive-color</code>, and nothing in the readout
945
+ stack does: <code>off</code> never reaches the root class list, and no
946
+ rule in <code>readout.css</code> or
947
+ <code>readout-block.css</code> styles it. The text stays at full value
948
+ colour — enhanced blue here rather than inactive grey. Unchanged by
949
+ the 6.1 review, which did not touch the off state.
950
+ </p>
951
+ <p>
952
+ The legacy <code>off-value</code> slot has no counterpart either;
953
+ <code>offText</code> takes a plain string only.
954
+ </p>
955
+ `,
956
+ }),
195
957
  };
196
958
 
197
959
  export const AlignMultiple: Story = {
198
- render: () => {
199
- return html`
200
- <div
201
- style="display: flex; flex-direction: column; align-items: end;
202
- --obc-instrument-field-source-width: 60px;
203
- --obc-instrument-field-tag-width: 5ch;
204
- "
205
- >
960
+ render: () =>
961
+ renderComparison({
962
+ name: 'Align Multiple',
963
+ verdict: Verdict.match,
964
+ legacy: html`
965
+ <div
966
+ style="display: flex; flex-direction: column; align-items: end;
967
+ --obc-instrument-field-source-width: 60px;
968
+ --obc-instrument-field-tag-width: 5ch;"
969
+ >
970
+ <obc-instrument-field
971
+ .size=${InstrumentFieldSize.enhanced}
972
+ horizontal
973
+ hasSetpoint
974
+ .setpoint=${360}
975
+ .value=${3}
976
+ .maxDigits=${3}
977
+ tag="HDG"
978
+ unit="DEG"
979
+ src="GPS"
980
+ hasSrc
981
+ ></obc-instrument-field>
982
+ <obc-instrument-field
983
+ .size=${InstrumentFieldSize.enhanced}
984
+ horizontal
985
+ hasSetpoint
986
+ .setpoint=${100}
987
+ .value=${100}
988
+ .maxDigits=${3}
989
+ tag="Depth"
990
+ unit="M"
991
+ hasSrc
992
+ src="GPS"
993
+ ></obc-instrument-field>
994
+ <obc-instrument-field
995
+ .size=${InstrumentFieldSize.enhanced}
996
+ horizontal
997
+ hasSetpoint
998
+ .setpoint=${10}
999
+ .value=${9}
1000
+ .maxDigits=${3}
1001
+ tag="STW"
1002
+ unit="KN"
1003
+ hasSrc
1004
+ src="GYRO 2"
1005
+ ></obc-instrument-field>
1006
+ </div>
1007
+ `,
1008
+ modern: html`
1009
+ <div style="display: flex; flex-direction: column; align-items: end;">
1010
+ <obc-readout
1011
+ .size=${ReadoutSize.large}
1012
+ .priority=${ReadoutPriority.enhanced}
1013
+ .direction=${ReadoutDirection.horizontal}
1014
+ hasSetpoint
1015
+ .setpointOptions=${{
1016
+ interaction: ReadoutSetpointInteraction.equalSize,
1017
+ }}
1018
+ .setpoint=${360}
1019
+ .value=${3}
1020
+ .maxDigits=${3}
1021
+ label="HDG"
1022
+ unit="DEG"
1023
+ src="GPS"
1024
+ .labelOptions=${{spaceReserver: 'Depth'}}
1025
+ .unitOptions=${{spaceReserver: 'DEG'}}
1026
+ .srcOptions=${{spaceReserver: 'GYRO 2'}}
1027
+ ></obc-readout>
1028
+ <obc-readout
1029
+ .size=${ReadoutSize.large}
1030
+ .priority=${ReadoutPriority.enhanced}
1031
+ .direction=${ReadoutDirection.horizontal}
1032
+ hasSetpoint
1033
+ .setpointOptions=${{
1034
+ interaction: ReadoutSetpointInteraction.equalSize,
1035
+ }}
1036
+ .setpoint=${100}
1037
+ .value=${100}
1038
+ .maxDigits=${3}
1039
+ label="Depth"
1040
+ unit="M"
1041
+ src="GPS"
1042
+ .labelOptions=${{spaceReserver: 'Depth'}}
1043
+ .unitOptions=${{spaceReserver: 'DEG'}}
1044
+ .srcOptions=${{spaceReserver: 'GYRO 2'}}
1045
+ ></obc-readout>
1046
+ <obc-readout
1047
+ .size=${ReadoutSize.large}
1048
+ .priority=${ReadoutPriority.enhanced}
1049
+ .direction=${ReadoutDirection.horizontal}
1050
+ hasSetpoint
1051
+ .setpointOptions=${{
1052
+ interaction: ReadoutSetpointInteraction.equalSize,
1053
+ }}
1054
+ .setpoint=${10}
1055
+ .value=${9}
1056
+ .maxDigits=${3}
1057
+ label="STW"
1058
+ unit="KN"
1059
+ src="GYRO 2"
1060
+ .labelOptions=${{spaceReserver: 'Depth'}}
1061
+ .unitOptions=${{spaceReserver: 'DEG'}}
1062
+ .srcOptions=${{spaceReserver: 'GYRO 2'}}
1063
+ ></obc-readout>
1064
+ </div>
1065
+ `,
1066
+ notes: html`
1067
+ <p>
1068
+ Legacy aligns a column of fields with two CSS variables:
1069
+ <code>--obc-instrument-field-tag-width</code> and
1070
+ <code>--obc-instrument-field-source-width</code>.
1071
+ </p>
1072
+ <p>
1073
+ <code>obc-readout</code> replaces them with per-block reservers, all
1074
+ set to the longest expected string on every row:
1075
+ <code>labelOptions.spaceReserver</code> (new in 6.1 — this closed the
1076
+ "no label reserver" gap that made the original comparison
1077
+ unreachable), <code>unitOptions.spaceReserver</code> and
1078
+ <code>srcOptions.spaceReserver</code>. With all three set, every
1079
+ column lines up and the setpoint edge stays square.
1080
+ </p>
1081
+ <p>
1082
+ For dense rows prefer <code>obc-readout-list</code> +
1083
+ <code>obc-readout-list-item</code>, which compute these reservers
1084
+ across rows automatically.
1085
+ </p>
1086
+ `,
1087
+ }),
1088
+ };
1089
+
1090
+ // ---------------------------------------------------------------------------
1091
+ // Edge cases the original stories never exercised. The pairs above can look
1092
+ // identical while the underlying semantics differ; these two stories pin the
1093
+ // divergence down so migrating consumers are not surprised by it.
1094
+ // ---------------------------------------------------------------------------
1095
+
1096
+ export const MaxDigitsWithDecimals: Story = {
1097
+ render: () =>
1098
+ renderComparison({
1099
+ name: 'Max Digits With Decimals (edge case)',
1100
+ verdict: Verdict.differs,
1101
+ legacy: html`
206
1102
  <obc-instrument-field
207
1103
  .size=${InstrumentFieldSize.enhanced}
208
- horizontal
209
- hasSetpoint
210
- .setpoint=${360}
211
- .value=${3}
212
- maxDigits="3"
213
- tag="HDG"
214
- unit="DEG"
215
- src="GPS"
216
- hasSrc
1104
+ .value=${1.5}
1105
+ .fractionDigits=${1}
1106
+ showZeroPadding
1107
+ .maxDigits=${4}
1108
+ tag="Offset"
1109
+ unit="m"
217
1110
  ></obc-instrument-field>
1111
+ `,
1112
+ modern: html`
1113
+ <obc-readout
1114
+ .size=${ReadoutSize.large}
1115
+ .priority=${ReadoutPriority.enhanced}
1116
+ .value=${1.5}
1117
+ .fractionDigits=${1}
1118
+ .maxDigits=${4}
1119
+ .valueOptions=${{hintedZeros: true}}
1120
+ label="Offset"
1121
+ unit="m"
1122
+ ></obc-readout>
1123
+ `,
1124
+ modernAlt: {
1125
+ tag: '<obc-readout> with translated maxDigits (4 − 1 − 1 = 2)',
1126
+ content: html`
1127
+ <obc-readout
1128
+ .size=${ReadoutSize.large}
1129
+ .priority=${ReadoutPriority.enhanced}
1130
+ .value=${1.5}
1131
+ .fractionDigits=${1}
1132
+ .maxDigits=${2}
1133
+ .valueOptions=${{hintedZeros: true}}
1134
+ label="Offset"
1135
+ unit="m"
1136
+ ></obc-readout>
1137
+ `,
1138
+ },
1139
+ notes: html`
1140
+ <p>
1141
+ The <em>Enhanced Zero Padding</em> pair looks identical only because
1142
+ its <code>fractionDigits</code> is 0. This story shows the divergence:
1143
+ legacy counts the <strong>whole formatted string</strong> against
1144
+ <code>maxDigits</code> (decimal point and fraction included), so
1145
+ <code>1.5</code> at <code>maxDigits: 4</code> pads to
1146
+ <code>01.5</code>. <code>obc-readout-block</code> counts
1147
+ <strong>integer digits only</strong>, so the same
1148
+ <code>maxDigits: 4</code> pads to <code>0001.5</code> (top right).
1149
+ </p>
1150
+ <p>
1151
+ To keep the rendering, translate the knob:
1152
+ <code
1153
+ >maxDigits<sub>readout</sub> = maxDigits<sub>legacy</sub> −
1154
+ fractionDigits − 1</code
1155
+ >
1156
+ (the −1 is the decimal point) — here <code>4 − 1 − 1 = 2</code>, shown
1157
+ underneath rendering <code>01.5</code> like the legacy side. Copying
1158
+ the number across unchanged over-pads.
1159
+ </p>
1160
+ `,
1161
+ }),
1162
+ };
1163
+
1164
+ export const AutoHideDeadband: Story = {
1165
+ render: () =>
1166
+ renderComparison({
1167
+ name: 'Auto Hide Deadband (edge case)',
1168
+ verdict: Verdict.gap,
1169
+ legacy: html`
218
1170
  <obc-instrument-field
1171
+ autoHideSetpoint
1172
+ .autoHideDeadband=${5}
219
1173
  .size=${InstrumentFieldSize.enhanced}
220
- horizontal
221
1174
  hasSetpoint
222
- .setpoint=${100}
223
- .value=${100}
224
- maxDigits="3"
225
- tag="Depth"
226
- unit="M"
227
- hasSrc
228
- src="GPS"
1175
+ .value=${8}
1176
+ .setpoint=${10}
1177
+ tag="HDG"
1178
+ unit="/min"
229
1179
  ></obc-instrument-field>
230
- <obc-instrument-field
231
- .size=${InstrumentFieldSize.enhanced}
232
- horizontal
1180
+ `,
1181
+ modern: html`
1182
+ <obc-readout
1183
+ .size=${ReadoutSize.large}
1184
+ .priority=${ReadoutPriority.enhanced}
233
1185
  hasSetpoint
1186
+ .value=${8}
234
1187
  .setpoint=${10}
235
- .value=${9}
236
- maxDigits="3"
237
- tag="STW"
238
- unit="KN"
239
- hasSrc
240
- src="GYRO 2"
241
- ></obc-instrument-field>
242
- </div>
243
- `;
244
- },
1188
+ .setpointOptions=${{
1189
+ interaction: ReadoutSetpointInteraction.popUp,
1190
+ }}
1191
+ label="HDG"
1192
+ unit="/min"
1193
+ ></obc-readout>
1194
+ `,
1195
+ notes: html`
1196
+ <p>
1197
+ The <em>Auto Hide Setpoint</em> pair agrees because its value sits
1198
+ exactly at the setpoint. This story shows where the semantics diverge:
1199
+ with <code>value: 8</code>, <code>setpoint: 10</code> and
1200
+ <code>autoHideDeadband: 5</code> the legacy side hides the setpoint
1201
+ (|10 − 8| ≤ 5) while the pop-up readout keeps it visible — the
1202
+ rendered strings <code>8</code> and <code>10</code> differ, and
1203
+ <code>isDisplayedAtSetpoint</code> compares nothing else.
1204
+ </p>
1205
+ <p>
1206
+ A deadband wider than the display rounding has
1207
+ <strong>no readout equivalent</strong>: pop-up's tolerance is exactly
1208
+ ±half of the last displayed digit (<code>fractionDigits: 0</code> →
1209
+ ±0.5). Consumers who relied on a wide deadband must either accept the
1210
+ tighter rendered-string rule or pre-round the value they feed the
1211
+ readout.
1212
+ </p>
1213
+ `,
1214
+ }),
245
1215
  };