@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.143 → 2.0.0-next.144

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 (33) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +129 -28
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +53 -19
  4. package/dist/components/app-button/app-button.css.js +56 -0
  5. package/dist/components/app-button/app-button.css.js.map +1 -1
  6. package/dist/integration-systems/integration-app-bar/integration-app-bar.css.js +12 -1
  7. package/dist/integration-systems/integration-app-bar/integration-app-bar.css.js.map +1 -1
  8. package/dist/integration-systems/integration-app-bar/integration-app-bar.d.ts +2 -2
  9. package/dist/integration-systems/integration-app-bar/integration-app-bar.d.ts.map +1 -1
  10. package/dist/integration-systems/integration-app-bar/integration-app-bar.js +1 -21
  11. package/dist/integration-systems/integration-app-bar/integration-app-bar.js.map +1 -1
  12. package/dist/navigation-instruments/readout/readout.d.ts +11 -2
  13. package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
  14. package/dist/navigation-instruments/readout/readout.js +18 -0
  15. package/dist/navigation-instruments/readout/readout.js.map +1 -1
  16. package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js +25 -0
  17. package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js.map +1 -1
  18. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts +20 -4
  19. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
  20. package/dist/navigation-instruments/readout-list-item/readout-list-item.js +18 -6
  21. package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/components/app-button/app-button.css +2 -0
  24. package/src/components/app-button/app-button.stories.ts +26 -0
  25. package/src/integration-systems/integration-app-bar/integration-app-bar-sizing.stories.ts +130 -0
  26. package/src/integration-systems/integration-app-bar/integration-app-bar.css +11 -1
  27. package/src/integration-systems/integration-app-bar/integration-app-bar.ts +3 -27
  28. package/src/navigation-instruments/readout/readout.stories.ts +57 -4
  29. package/src/navigation-instruments/readout/readout.ts +28 -1
  30. package/src/navigation-instruments/readout-list/readout-list.stories.ts +62 -2
  31. package/src/navigation-instruments/readout-list-item/readout-list-item.css +21 -0
  32. package/src/navigation-instruments/readout-list-item/readout-list-item.stories.ts +56 -3
  33. package/src/navigation-instruments/readout-list-item/readout-list-item.ts +37 -19
@@ -1,6 +1,6 @@
1
1
  import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
2
  import {html, nothing} from 'lit';
3
- import {userEvent} from 'storybook/test';
3
+ import {expect, spyOn, userEvent} from 'storybook/test';
4
4
  import {gsap} from 'gsap';
5
5
  import './readout.js';
6
6
  import '../../components/navigation-item/navigation-item.js';
@@ -1587,20 +1587,17 @@ export const TestCases: Story = {
1587
1587
  </style>
1588
1588
  <div class="test-case-container">
1589
1589
  <obc-readout
1590
- .size=${ReadoutSize.small}
1591
1590
  .direction=${ReadoutDirection.horizontal}
1592
1591
  .value=${123}
1593
1592
  .unit=${'kn'}
1594
1593
  ></obc-readout>
1595
1594
  <obc-readout
1596
- .size=${ReadoutSize.small}
1597
1595
  .direction=${ReadoutDirection.horizontal}
1598
1596
  .value=${'Thermo On'}
1599
1597
  .valueType=${ReadoutValueType.text}
1600
1598
  .label=${'Operating mode'}
1601
1599
  ></obc-readout>
1602
1600
  <obc-readout
1603
- .size=${ReadoutSize.small}
1604
1601
  .direction=${ReadoutDirection.horizontal}
1605
1602
  .value=${'Normal'}
1606
1603
  .valueType=${ReadoutValueType.text}
@@ -1610,6 +1607,62 @@ export const TestCases: Story = {
1610
1607
  },
1611
1608
  };
1612
1609
 
1610
+ /**
1611
+ * A horizontal readout given a `size` other than `large` warns once per
1612
+ * element. The tier is discarded (the arrangement exists in the large tier
1613
+ * only), and a silent discard is indistinguishable from a bug to a consumer
1614
+ * (#1182). A removed `size` attribute arrives as `null` through Lit's String
1615
+ * converter and counts as unset. Driven through `willUpdate` on detached
1616
+ * elements, like `Readout Block → TestValidationSurvivesUnrelatedUpdate`.
1617
+ */
1618
+ export const TestHorizontalSizeWarning: Story = {
1619
+ render: () => html`<span>Regression test — see the play function.</span>`,
1620
+ play: async () => {
1621
+ type Probe = HTMLElement & {
1622
+ size?: ReadoutSize;
1623
+ direction?: ReadoutDirection;
1624
+ willUpdate: (changed: Map<string, unknown>) => void;
1625
+ };
1626
+ const probe = (
1627
+ size: ReadoutSize | undefined,
1628
+ direction: ReadoutDirection
1629
+ ) => {
1630
+ const el = document.createElement('obc-readout') as Probe;
1631
+ el.size = size;
1632
+ el.direction = direction;
1633
+ return el;
1634
+ };
1635
+ const update = (el: Probe) => el.willUpdate(new Map());
1636
+ const warn = spyOn(console, 'warn').mockImplementation(() => undefined);
1637
+ try {
1638
+ update(probe(ReadoutSize.large, ReadoutDirection.horizontal));
1639
+ update(probe(ReadoutSize.small, ReadoutDirection.vertical));
1640
+ update(probe(undefined, ReadoutDirection.horizontal));
1641
+ const removed = probe(ReadoutSize.small, ReadoutDirection.horizontal);
1642
+ removed.setAttribute('size', ReadoutSize.small);
1643
+ removed.removeAttribute('size');
1644
+ update(removed);
1645
+ await expect(warn).not.toHaveBeenCalled();
1646
+
1647
+ const small = probe(ReadoutSize.small, ReadoutDirection.horizontal);
1648
+ update(small);
1649
+ await expect(warn).toHaveBeenCalledTimes(1);
1650
+ await expect(warn.mock.calls[0][0]).toMatch(
1651
+ /\[obc-readout\].*size.*horizontal/
1652
+ );
1653
+ await expect(warn.mock.calls[0][1]).toBe(small);
1654
+
1655
+ update(small);
1656
+ await expect(warn).toHaveBeenCalledTimes(1);
1657
+
1658
+ update(probe(ReadoutSize.medium, ReadoutDirection.horizontal));
1659
+ await expect(warn).toHaveBeenCalledTimes(2);
1660
+ } finally {
1661
+ warn.mockRestore();
1662
+ }
1663
+ },
1664
+ };
1665
+
1613
1666
  /**
1614
1667
  * Figma 6.1 "Equal size": value and setpoint always share the primary size —
1615
1668
  * the missing fourth mode beside primary-secondary (`always-visible`),
@@ -249,7 +249,8 @@ export interface ReadoutSourceOptions extends ReadoutSrcOptions {
249
249
  * @property size - Density tier. Applies to the vertical direction only — the horizontal
250
250
  * arrangement exists in the large tier alone (Figma 6.1: it relies on the
251
251
  * label+unit stack aligning with the L-size value caps), so `size` is
252
- * ignored when `direction` is `horizontal`.
252
+ * ignored when `direction` is `horizontal`; a horizontal readout given any
253
+ * other `size` logs a console warning once per element.
253
254
  * @availableWhen size direction==vertical
254
255
  * @availableWhen stacking direction==vertical
255
256
  * @availableWhen alignment direction==vertical && stacking==stacked
@@ -365,6 +366,7 @@ export class ObcReadout extends LitElement {
365
366
  ReadoutBlockHidePhase.none;
366
367
  private deferredSetpointHideTimer?: number;
367
368
  private hasCompletedFirstUpdate = false;
369
+ private hasWarnedHorizontalSize = false;
368
370
 
369
371
  @state() private sourcePickerContentVisible = false;
370
372
  @state() private sourcePickerOptions: ContextMenuOption[] = [];
@@ -1310,6 +1312,31 @@ export class ObcReadout extends LitElement {
1310
1312
  // exactly the silent failure this assertion exists to prevent.
1311
1313
  assertReadoutValueType('obc-readout', this.value, this.valueType);
1312
1314
  assertReadoutFractionDigits('obc-readout', this.fractionDigits);
1315
+ this.warnHorizontalSizeIgnored();
1316
+ }
1317
+
1318
+ /**
1319
+ * A horizontal readout discards any `size` but `large` (see `resolvedSize`).
1320
+ * A silent discard is indistinguishable from a bug to a consumer, so each
1321
+ * offending element says so once and logs itself for locating (#1182).
1322
+ * A removed `size` attribute arrives as `null` and counts as unset.
1323
+ */
1324
+ private warnHorizontalSizeIgnored(): void {
1325
+ if (
1326
+ this.hasWarnedHorizontalSize ||
1327
+ !this.isHorizontal ||
1328
+ !this.size ||
1329
+ this.size === ReadoutSize.large
1330
+ ) {
1331
+ return;
1332
+ }
1333
+ this.hasWarnedHorizontalSize = true;
1334
+ console.warn(
1335
+ `[obc-readout] size="${this.size}" is ignored when direction="horizontal": ` +
1336
+ 'the horizontal arrangement exists in the large tier only. Remove the ' +
1337
+ 'size, or use direction="vertical" for the small / medium tiers.',
1338
+ this
1339
+ );
1313
1340
  }
1314
1341
 
1315
1342
  override updated(changed: Map<string, unknown>): void {
@@ -6,9 +6,12 @@ import {
6
6
  ReadoutListItemSize,
7
7
  ReadoutListItemPriority,
8
8
  ReadoutListItemDataQuality,
9
+ ReadoutListItemStacking,
10
+ ObcTextboxFontWeight,
9
11
  type ReadoutValueOptions,
10
12
  ReadoutValueType,
11
13
  } from '../readout-list-item/readout-list-item.js';
14
+ import {ObcTextboxSize} from '../../components/textbox/textbox.js';
12
15
  import '../readout-list-item/readout-list-item.js';
13
16
  import type {AlertFrameConfig} from '../../components/alert-frame/alert-frame.js';
14
17
  import {
@@ -23,13 +26,17 @@ type ListArgs = {
23
26
 
24
27
  type Row = {
25
28
  label: string;
29
+ src?: string;
26
30
  value: number | string | null;
27
31
  valueType?: ReadoutValueType;
28
32
  unit: string;
29
33
  size?: ReadoutListItemSize;
34
+ stacking?: ReadoutListItemStacking;
35
+ labelSize?: ObcTextboxSize;
30
36
  hasDegree?: boolean;
31
37
  fractionDigits?: number;
32
38
  priority?: ReadoutListItemPriority;
39
+ weight?: ObcTextboxFontWeight;
33
40
  hasSetpoint?: boolean;
34
41
  setpoint?: number;
35
42
  hasAdvice?: boolean;
@@ -46,8 +53,12 @@ type Row = {
46
53
 
47
54
  function renderRow(row: Row) {
48
55
  const valueOptions =
49
- row.valueDataQuality || row.valueAlert
50
- ? {dataQuality: row.valueDataQuality, alert: row.valueAlert}
56
+ row.valueDataQuality || row.valueAlert || row.weight
57
+ ? {
58
+ dataQuality: row.valueDataQuality,
59
+ alert: row.valueAlert,
60
+ weight: row.weight,
61
+ }
51
62
  : undefined;
52
63
  const setpointOptions = row.setpointDataQuality
53
64
  ? {dataQuality: row.setpointDataQuality}
@@ -55,10 +66,13 @@ function renderRow(row: Row) {
55
66
  return html`
56
67
  <obc-readout-list-item
57
68
  .label=${row.label}
69
+ .src=${row.src}
58
70
  .unit=${row.unit}
59
71
  .value=${row.value}
60
72
  .valueType=${row.valueType ?? ReadoutValueType.number}
61
73
  .size=${row.size ?? ReadoutListItemSize.small}
74
+ .stacking=${row.stacking}
75
+ .labelOptions=${row.labelSize ? {size: row.labelSize} : undefined}
62
76
  .hasDegree=${row.hasDegree ?? false}
63
77
  .fractionDigits=${row.fractionDigits ?? 0}
64
78
  .priority=${row.priority}
@@ -169,6 +183,52 @@ export const Degrees: Story = {
169
183
  render: (args) => renderList(DEGREE_ROWS, args.showDebugOverlay),
170
184
  };
171
185
 
186
+ /**
187
+ * **Source on the label's line.** `stacking="leading-src-inline"` keeps each
188
+ * row one line high with the source after the label (Figma 46596:102880 pairs
189
+ * an `s` label with the `xs` source). Inside a list the source column is
190
+ * reserved to the longest source, so the readings stay aligned across rows
191
+ * that have no source at all. The unit column is likewise reserved to the
192
+ * longest unit (`/min`); standalone rows let each unit hug its value instead
193
+ * (see `Readout List Item → ColumnAlignment`, "without reservers").
194
+ */
195
+ const INLINE_SOURCE_ROWS: Row[] = [
196
+ {
197
+ label: 'HDG',
198
+ src: 'GPS1',
199
+ value: 355,
200
+ unit: '',
201
+ hasDegree: true,
202
+ priority: ReadoutListItemPriority.enhanced,
203
+ weight: ObcTextboxFontWeight.semibold,
204
+ },
205
+ {label: 'COG', src: 'POS1', value: 5, unit: '', hasDegree: true},
206
+ {label: 'ROT', src: 'GYRO2', value: 10, unit: '/min', hasDegree: true},
207
+ {
208
+ label: 'SOG',
209
+ src: 'LOG1',
210
+ value: 9.8,
211
+ unit: 'kn',
212
+ fractionDigits: 1,
213
+ priority: ReadoutListItemPriority.enhanced,
214
+ weight: ObcTextboxFontWeight.semibold,
215
+ },
216
+ {label: 'STW', src: 'GPS1', value: 10.2, unit: 'kn', fractionDigits: 1},
217
+ {label: 'DPTH', src: 'ECHO1', value: 534, unit: 'm'},
218
+ {label: 'Pitch', value: 5.3, unit: '', hasDegree: true, fractionDigits: 1},
219
+ {label: 'Draft fore', value: 3.78, unit: 'm', fractionDigits: 2},
220
+ ].map((row) => ({
221
+ ...row,
222
+ size: ReadoutListItemSize.medium,
223
+ stacking: ReadoutListItemStacking.leadingSrcInline,
224
+ labelSize: ObcTextboxSize.s,
225
+ }));
226
+
227
+ export const LeadingSrcInline: Story = {
228
+ args: {showDebugOverlay: false},
229
+ render: (args) => renderList(INLINE_SOURCE_ROWS, args.showDebugOverlay),
230
+ };
231
+
172
232
  const WARNING_FRAME: AlertFrameConfig = {
173
233
  status: AlertType.Warning,
174
234
  mode: ObcAlertFrameMode.ackedActive,
@@ -550,6 +550,27 @@ obc-readout-block {
550
550
  gap: var(--global-size-spacing-target-padding);
551
551
  }
552
552
 
553
+ /* leading-src-inline: the source follows the label on one line, bottom-aligned
554
+ so the `xs` source and the label share a cap baseline. The label↔source gap
555
+ reuses the tier's content gap (8px, matching Figma 46596:102880). */
556
+ .stacking-leading-src-inline .label-stack {
557
+ flex-direction: row;
558
+ align-items: flex-end;
559
+ }
560
+ .size-small.stacking-leading-src-inline .label-stack {
561
+ gap: var(
562
+ --instrument-components-readout-new-list-item-regular-content-container-gap
563
+ );
564
+ }
565
+ .size-medium.stacking-leading-src-inline .label-stack {
566
+ gap: var(
567
+ --instrument-components-readout-new-list-item-medium-content-container-gap
568
+ );
569
+ }
570
+ .size-large.stacking-leading-src-inline .label-stack {
571
+ gap: var(--instrument-components-readout-new-list-item-large-content-gap);
572
+ }
573
+
553
574
  /* ---------- Data quality ----------
554
575
  * Orthogonal to the alert frame — both can apply at once. */
555
576
  .root.data-low-integrity .surface,
@@ -726,6 +726,42 @@ export const LeadingSrc: Story = {
726
726
  ]),
727
727
  };
728
728
 
729
+ /**
730
+ * `leading-src-inline` keeps the source on the label's line, so a row with a
731
+ * source stays one line high (Figma 46596:102880 pairs an `s` label with the
732
+ * `xs` source; the dense default label stays `xs` unless `labelOptions.size`
733
+ * says otherwise).
734
+ */
735
+ export const LeadingSrcInline: Story = {
736
+ render: () =>
737
+ renderShowcase([
738
+ {
739
+ title: 'Stacking: leading-src-inline',
740
+ columns: 3,
741
+ cases: stackingCases(ReadoutListItemStacking.leadingSrcInline),
742
+ },
743
+ {
744
+ title: 'Label s + source xs (Figma 46596:102880)',
745
+ columns: 3,
746
+ cases: SIZES.map((size) => ({
747
+ label: `${size} / label s`,
748
+ config: {
749
+ label: 'HDG',
750
+ src: 'GPS1',
751
+ value: 355,
752
+ unit: '',
753
+ options: {
754
+ size,
755
+ stacking: ReadoutListItemStacking.leadingSrcInline,
756
+ hasDegree: true,
757
+ label: {size: ObcTextboxSize.s},
758
+ },
759
+ },
760
+ })),
761
+ },
762
+ ]),
763
+ };
764
+
729
765
  export const SetpointFlipFlop: Story = {
730
766
  render: () =>
731
767
  renderShowcase([
@@ -1781,8 +1817,12 @@ export const MissingParts: Story = {
1781
1817
  * same width regardless of each row's own value length / `fractionDigits`, and
1782
1818
  * the columns line up.
1783
1819
  *
1784
- * (Source/stacking variations are exercised in the `LeadingSrc` / `LeadingUnit`
1785
- * stories; mixing them here would move the unit out of the rightmost column.)
1820
+ * The Heading row has no unit and carries its source inline
1821
+ * (`leading-src-inline`): in the aligned column it still renders the blank
1822
+ * reserved unit column, so its degree lines up with the rows that have a unit.
1823
+ * (`leading-src` and `leading-unit` are exercised in their own stories:
1824
+ * `leading-unit` would move the unit out of the rightmost column, and
1825
+ * `leading-src` would make the row two lines high.)
1786
1826
  *
1787
1827
  * The last two rows use `size=medium` / `size=large`. Their value digit edges do
1788
1828
  * NOT fully align with the small rows (~8px stagger): the `°` column scales with
@@ -1800,10 +1840,12 @@ export const MissingParts: Story = {
1800
1840
  */
1801
1841
  type AlignmentRow = {
1802
1842
  label: string;
1843
+ src?: string;
1803
1844
  value: number | string | null;
1804
1845
  valueType?: ReadoutValueType;
1805
1846
  unit: string;
1806
1847
  size?: ReadoutListItemSize;
1848
+ stacking?: ReadoutListItemStacking;
1807
1849
  hasDegree?: boolean;
1808
1850
  fractionDigits?: number;
1809
1851
  priority?: ReadoutListItemPriority;
@@ -1849,6 +1891,16 @@ const ALIGNMENT_ROWS: AlignmentRow[] = [
1849
1891
  // per-block advice low-integrity — the advice chip must not shift the columns
1850
1892
  adviceDataQuality: ReadoutListItemDataQuality.lowIntegrity,
1851
1893
  },
1894
+ // no unit + degree + inline source — the blank reserved unit column keeps
1895
+ // the degree glyph aligned with the unit rows
1896
+ {
1897
+ label: 'Heading',
1898
+ src: 'GPS1',
1899
+ value: 355,
1900
+ unit: '',
1901
+ hasDegree: true,
1902
+ stacking: ReadoutListItemStacking.leadingSrcInline,
1903
+ },
1852
1904
  // negative value + fraction + low-integrity data quality
1853
1905
  {
1854
1906
  label: 'Flow speed',
@@ -1987,7 +2039,7 @@ function renderAlignmentColumn(aligned: boolean, showDebugOverlay: boolean) {
1987
2039
  renderItem({
1988
2040
  label: row.label,
1989
2041
  unit: row.unit,
1990
- src: '',
2042
+ src: row.src ?? '',
1991
2043
  value: row.value,
1992
2044
  valueType: row.valueType,
1993
2045
  hasSetpoint: row.hasSetpoint,
@@ -1999,6 +2051,7 @@ function renderAlignmentColumn(aligned: boolean, showDebugOverlay: boolean) {
1999
2051
  showDebugOverlay,
2000
2052
  options: {
2001
2053
  size: row.size ?? ReadoutListItemSize.small,
2054
+ stacking: row.stacking,
2002
2055
  hasDegree: row.hasDegree ?? false,
2003
2056
  fractionDigits: row.fractionDigits ?? 0,
2004
2057
  priority: row.priority,
@@ -71,12 +71,15 @@ export type ReadoutListItemSize = ReadoutBlockSize;
71
71
  * Placement of the unit/source relative to the label and value.
72
72
  * - `trailing-unit`: unit after the value, source after a trailing divider.
73
73
  * - `leading-unit`: unit beside/under the label.
74
- * - `leading-src`: source beside/under the label (no trailing source).
74
+ * - `leading-src`: source under the label (no trailing source).
75
+ * - `leading-src-inline`: source on the label's line, after it (no trailing
76
+ * source); keeps the row one line high.
75
77
  */
76
78
  export enum ReadoutListItemStacking {
77
79
  trailingUnit = 'trailing-unit',
78
80
  leadingUnit = 'leading-unit',
79
81
  leadingSrc = 'leading-src',
82
+ leadingSrcInline = 'leading-src-inline',
80
83
  }
81
84
 
82
85
  /**
@@ -208,7 +211,12 @@ export interface ReadoutAdviceOptions extends ReadoutBlockState {
208
211
  }
209
212
 
210
213
  export interface ReadoutReserverOptions {
211
- /** Longest expected string to reserve width for (aligns multiple rows), e.g. `"miles"`. */
214
+ /**
215
+ * Longest expected string to reserve width for (aligns multiple rows), e.g.
216
+ * `"miles"`. A row without a unit still renders the reserved (blank)
217
+ * trailing column, so its value and degree stay on the grid of the rows
218
+ * that have one. `leading-unit` stacking has no trailing column to reserve.
219
+ */
212
220
  spaceReserver?: string;
213
221
  }
214
222
 
@@ -273,7 +281,8 @@ export interface ReadoutSrcOptions extends ReadoutBlockState {
273
281
  * - **Building blocks:** value, optional setpoint, and optional advice segments,
274
282
  * each cap-height-aligned and able to reserve a stable width.
275
283
  * - **Sizes:** `small`, `medium`, `large` density scales.
276
- * - **Stacking:** `trailing-unit`, `leading-unit`, `leading-src` placement.
284
+ * - **Stacking:** `trailing-unit`, `leading-unit`, `leading-src`,
285
+ * `leading-src-inline` placement.
277
286
  * - **Priority:** `regular`/`enhanced` colour emphasis; per-value `weight`
278
287
  * (`regular`/`semibold`/`bold`) is independent of colour.
279
288
  * - **Setpoint flip-flop:** swaps emphasis between value and setpoint as the
@@ -406,10 +415,30 @@ export class ObcReadoutListItem extends LitElement {
406
415
  return this.stacking ?? ReadoutListItemStacking.trailingUnit;
407
416
  }
408
417
 
418
+ /** Both leading-src stackings move the source into the label stack. */
419
+ private get hasLeadingSrc(): boolean {
420
+ const stacking = this.resolvedStacking;
421
+ return (
422
+ stacking === ReadoutListItemStacking.leadingSrc ||
423
+ stacking === ReadoutListItemStacking.leadingSrcInline
424
+ );
425
+ }
426
+
409
427
  private get resolvedPriority(): ReadoutListItemPriority {
410
428
  return this.priority ?? ReadoutListItemPriority.regular;
411
429
  }
412
430
 
431
+ /**
432
+ * Whether a unit box follows the value: a unit, or a reserver alone — the
433
+ * blank reserved column keeps a unit-less row aligned with its neighbours.
434
+ */
435
+ private get hasTrailingUnitBox(): boolean {
436
+ return (
437
+ this.resolvedStacking !== ReadoutListItemStacking.leadingUnit &&
438
+ (Boolean(this.unit) || Boolean(this.unitOptions?.spaceReserver))
439
+ );
440
+ }
441
+
413
442
  private get resolvedFractionDigits(): number {
414
443
  return this.fractionDigits ?? 0;
415
444
  }
@@ -713,16 +742,12 @@ export class ObcReadoutListItem extends LitElement {
713
742
  if (!this.hasValue) {
714
743
  return nothing;
715
744
  }
716
- const hasTrailingUnit =
717
- Boolean(this.unit) &&
718
- this.resolvedStacking !== ReadoutListItemStacking.leadingUnit;
719
-
720
745
  if ((this.hasDegree ?? false) && !this.off) {
721
746
  return this.renderDegreeGlyph(this.valueSize, {
722
747
  enhanced: this.rowEnhanced,
723
748
  });
724
749
  }
725
- if (hasTrailingUnit) {
750
+ if (this.hasTrailingUnitBox) {
726
751
  return html`<span class="value-unit-gap" aria-hidden="true"></span>`;
727
752
  }
728
753
  return nothing;
@@ -914,8 +939,7 @@ export class ObcReadoutListItem extends LitElement {
914
939
  const stacking = this.resolvedStacking;
915
940
  const showLeadingUnit =
916
941
  stacking === ReadoutListItemStacking.leadingUnit && Boolean(this.unit);
917
- const showLeadingSrc =
918
- stacking === ReadoutListItemStacking.leadingSrc && Boolean(this.src);
942
+ const showLeadingSrc = this.hasLeadingSrc && Boolean(this.src);
919
943
 
920
944
  return html`
921
945
  <div class="label-container" part="label-container">
@@ -946,15 +970,12 @@ export class ObcReadoutListItem extends LitElement {
946
970
  }
947
971
 
948
972
  private renderTrailingUnit(): TemplateResult | typeof nothing {
949
- if (
950
- this.resolvedStacking === ReadoutListItemStacking.leadingUnit ||
951
- !this.unit
952
- ) {
973
+ if (!this.hasTrailingUnitBox) {
953
974
  return nothing;
954
975
  }
955
976
  return this.renderTextbox(
956
977
  'unit',
957
- this.unit,
978
+ this.unit ?? '',
958
979
  this.unitOptions?.spaceReserver
959
980
  );
960
981
  }
@@ -1004,10 +1025,7 @@ export class ObcReadoutListItem extends LitElement {
1004
1025
  }
1005
1026
 
1006
1027
  private renderTrailingSource(): TemplateResult | typeof nothing {
1007
- if (
1008
- this.resolvedStacking === ReadoutListItemStacking.leadingSrc ||
1009
- !this.src
1010
- ) {
1028
+ if (this.hasLeadingSrc || !this.src) {
1011
1029
  return nothing;
1012
1030
  }
1013
1031
  return html`