@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.89 → 2.0.0-next.90

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.
@@ -5,9 +5,16 @@ import './readout-list.js';
5
5
  import {
6
6
  ReadoutListItemSize,
7
7
  ReadoutListItemPriority,
8
+ ReadoutListItemDataQuality,
8
9
  type ReadoutValueOptions,
9
10
  } from '../readout-list-item/readout-list-item.js';
10
11
  import '../readout-list-item/readout-list-item.js';
12
+ import type {AlertFrameConfig} from '../../components/alert-frame/alert-frame.js';
13
+ import {
14
+ ObcAlertFrameMode,
15
+ ObcAlertFrameType,
16
+ } from '../../components/alert-frame/alert-frame.js';
17
+ import {AlertType} from '../../types.js';
11
18
 
12
19
  type ListArgs = {
13
20
  showDebugOverlay: boolean;
@@ -23,9 +30,26 @@ type Row = {
23
30
  priority?: ReadoutListItemPriority;
24
31
  hasSetpoint?: boolean;
25
32
  setpoint?: number;
33
+ hasAdvice?: boolean;
34
+ advice?: number;
35
+ off?: boolean;
36
+ // Row-level and per-block state, so the list stories can smoke-test data
37
+ // quality + alert frames alongside the auto-alignment.
38
+ dataQuality?: ReadoutListItemDataQuality;
39
+ alert?: AlertFrameConfig;
40
+ valueDataQuality?: ReadoutListItemDataQuality;
41
+ valueAlert?: AlertFrameConfig;
42
+ setpointDataQuality?: ReadoutListItemDataQuality;
26
43
  };
27
44
 
28
45
  function renderRow(row: Row) {
46
+ const valueOptions =
47
+ row.valueDataQuality || row.valueAlert
48
+ ? {dataQuality: row.valueDataQuality, alert: row.valueAlert}
49
+ : undefined;
50
+ const setpointOptions = row.setpointDataQuality
51
+ ? {dataQuality: row.setpointDataQuality}
52
+ : undefined;
29
53
  return html`
30
54
  <obc-readout-list-item
31
55
  .label=${row.label}
@@ -35,8 +59,15 @@ function renderRow(row: Row) {
35
59
  .hasDegree=${row.hasDegree ?? false}
36
60
  .fractionDigits=${row.fractionDigits ?? 0}
37
61
  .priority=${row.priority}
62
+ .off=${row.off ?? false}
38
63
  .hasSetpoint=${row.hasSetpoint ?? false}
39
64
  .setpoint=${row.setpoint}
65
+ .hasAdvice=${row.hasAdvice ?? false}
66
+ .advice=${row.advice}
67
+ .dataQuality=${row.dataQuality}
68
+ .alert=${row.alert ?? false}
69
+ .valueOptions=${valueOptions}
70
+ .setpointOptions=${setpointOptions}
40
71
  ></obc-readout-list-item>
41
72
  `;
42
73
  }
@@ -92,6 +123,18 @@ export const Degrees: Story = {
92
123
  render: (args) => renderList(DEGREE_ROWS, args.showDebugOverlay),
93
124
  };
94
125
 
126
+ const WARNING_FRAME: AlertFrameConfig = {
127
+ status: AlertType.Warning,
128
+ mode: ObcAlertFrameMode.ackedActive,
129
+ type: ObcAlertFrameType.Regular,
130
+ };
131
+ const ALARM_FRAME_ICON: AlertFrameConfig = {
132
+ status: AlertType.Alarm,
133
+ mode: ObcAlertFrameMode.ackedActive,
134
+ type: ObcAlertFrameType.SmallSideFlip,
135
+ showAlertCategoryIcon: true,
136
+ };
137
+
95
138
  export const WithSetpoints: Story = {
96
139
  render: (args) =>
97
140
  renderList(
@@ -104,6 +147,7 @@ export const WithSetpoints: Story = {
104
147
  hasSetpoint: true,
105
148
  setpoint: 290,
106
149
  },
150
+ // invalid setpoint chip — auto-aligned alongside the others
107
151
  {
108
152
  label: 'Speed',
109
153
  value: 8,
@@ -111,8 +155,28 @@ export const WithSetpoints: Story = {
111
155
  fractionDigits: 1,
112
156
  hasSetpoint: true,
113
157
  setpoint: 12,
158
+ setpointDataQuality: ReadoutListItemDataQuality.invalid,
159
+ },
160
+ // advice reference
161
+ {label: 'Depth', value: 1013, unit: 'm', hasAdvice: true, advice: 1010},
162
+ // per-value alert frame — wraps value + unit
163
+ {label: 'Fuel', value: 42, unit: '%', valueAlert: WARNING_FRAME},
164
+ // whole row invalid + null value (dash)
165
+ {
166
+ label: 'Wind',
167
+ value: null,
168
+ unit: 'kn',
169
+ dataQuality: ReadoutListItemDataQuality.invalid,
170
+ },
171
+ // whole row low-integrity + row-level alert frame with a badge-icon flap
172
+ {
173
+ label: 'Course',
174
+ value: 120,
175
+ unit: 'T',
176
+ hasDegree: true,
177
+ dataQuality: ReadoutListItemDataQuality.lowIntegrity,
178
+ alert: ALARM_FRAME_ICON,
114
179
  },
115
- {label: 'Depth', value: 1013, unit: 'm'},
116
180
  ],
117
181
  args.showDebugOverlay
118
182
  ),
@@ -140,13 +204,47 @@ export const TestDynamicRow: Story = {
140
204
  },
141
205
  };
142
206
 
207
+ // Richer starting set for the interactive demo: a plain degree row, an invalid
208
+ // setpoint, an advice reference, a per-value alert frame, a null/invalid row and
209
+ // a low-integrity row with a row-level alert frame — so the manual add/remove /
210
+ // re-align flow is exercised against the full range of state.
211
+ const MANUAL_ROWS: Row[] = [
212
+ {label: 'Heading', value: 287, unit: 'T', hasDegree: true},
213
+ {
214
+ label: 'Speed',
215
+ value: 8,
216
+ unit: 'kn',
217
+ fractionDigits: 1,
218
+ hasSetpoint: true,
219
+ setpoint: 12,
220
+ setpointDataQuality: ReadoutListItemDataQuality.invalid,
221
+ },
222
+ {label: 'Depth', value: 1013, unit: 'm', hasAdvice: true, advice: 1010},
223
+ {label: 'Fuel', value: 42, unit: '%', valueAlert: WARNING_FRAME},
224
+ {
225
+ label: 'Wind',
226
+ value: null,
227
+ unit: 'kn',
228
+ dataQuality: ReadoutListItemDataQuality.invalid,
229
+ },
230
+ {
231
+ label: 'Course',
232
+ value: 120,
233
+ unit: 'T',
234
+ hasDegree: true,
235
+ dataQuality: ReadoutListItemDataQuality.lowIntegrity,
236
+ alert: ALARM_FRAME_ICON,
237
+ },
238
+ ];
239
+
143
240
  /**
144
241
  * **Manual (Interactive)** — the rows are slotted children, so they cannot be
145
242
  * driven by Storybook controls. Use the buttons to add / remove rows (a
146
243
  * structural change the list re-aligns automatically) and to change the first
147
244
  * row's value / unit (a property-only change, so it calls the public
148
245
  * {@link ObcReadoutList.align} method). The debug overlay shows the reserved
149
- * column widths reacting to each change.
246
+ * column widths reacting to each change. The starting rows include data-quality
247
+ * and alert-frame variations for a quick visual smoke test.
150
248
  */
151
249
  export const Manual: Story = {
152
250
  name: 'Manual (Interactive)',
@@ -168,22 +266,7 @@ export const Manual: Story = {
168
266
  id="manual-list"
169
267
  .showDebugOverlay=${args.showDebugOverlay}
170
268
  >
171
- <obc-readout-list-item
172
- .label=${'Heading'}
173
- .unit=${'T'}
174
- .value=${287}
175
- .hasDegree=${true}
176
- ></obc-readout-list-item>
177
- <obc-readout-list-item
178
- .label=${'Speed'}
179
- .unit=${'kn'}
180
- .value=${18}
181
- ></obc-readout-list-item>
182
- <obc-readout-list-item
183
- .label=${'Depth'}
184
- .unit=${'m'}
185
- .value=${124}
186
- ></obc-readout-list-item>
269
+ ${MANUAL_ROWS.map(renderRow)}
187
270
  </obc-readout-list>
188
271
  <div style="display: flex; gap: 8px; flex-wrap: wrap;">
189
272
  <button id="m-add" style="padding: 6px 12px; cursor: pointer;">
@@ -203,7 +286,7 @@ export const Manual: Story = {
203
286
  id="m-status"
204
287
  style="font: 12px/1.4 monospace; color: var(--element-neutral-color, #666);"
205
288
  >
206
- 3 rows
289
+ ${MANUAL_ROWS.length} rows
207
290
  </div>
208
291
  </div>
209
292
  `,
@@ -189,6 +189,80 @@ obc-readout-block {
189
189
  align-items: flex-end;
190
190
  }
191
191
 
192
+ /* ---------- Value reading (value + degree + unit) ----------
193
+ * Groups the value block, its degree column and the trailing unit so the value
194
+ * alert frame can wrap all three. Layout-neutral: it adds no gap, so the content
195
+ * sits exactly where it did when these were siblings of `.value-area`. */
196
+ .value-reading {
197
+ position: relative;
198
+ display: inline-flex;
199
+ align-items: flex-end;
200
+ }
201
+
202
+ /* ---------- Value data quality (low-integrity / invalid) ----------
203
+ * The value's per-block data-quality chip, applied to the whole reading (value +
204
+ * degree + unit) rather than hugging the value alone — the same logic as the
205
+ * value alert frame, minus its custom padding. Same look as the block/source
206
+ * chip (1px outline, check radius, alert-quality colours). Uses `outline` (not
207
+ * `border`) so it never shifts the value's width / cross-row column alignment.
208
+ * Setpoint/advice keep their own in-block chip; the row-level data quality on
209
+ * `.surface` is unchanged. */
210
+ .value-reading.data-low-integrity,
211
+ .value-reading.data-invalid {
212
+ outline: 1px solid;
213
+ border-radius: var(--global-border-radius-border-radius-check);
214
+ }
215
+ .value-reading.data-low-integrity {
216
+ background: var(--alert-low-integrity-background-color);
217
+ outline-color: var(--alert-low-integrity-border-color);
218
+ }
219
+ .value-reading.data-invalid {
220
+ background: var(--alert-invalid-background-color);
221
+ outline-color: var(--alert-invalid-border-color);
222
+ }
223
+
224
+ /* ---------- Value alert overlay ----------
225
+ * A pure overlay around the value reading: it reserves no space, so toggling the
226
+ * frame never changes the row height or breaks cross-row column alignment. The
227
+ * box is offset outward by (padding − stroke/2) on each axis so the alert-frame's
228
+ * outline (drawn outside its box) lands CENTRED on the 4px/2px padding line
229
+ * (half inside / half outside), matching Figma 58:10120. */
230
+ .value-alert-overlay {
231
+ --_obc-readout-value-frame-padding-horizontal: 4px;
232
+ --_obc-readout-value-frame-padding-vertical: 2px;
233
+ /* Must match obc-alert-frame's ACTUAL outline width so the stroke stays centred
234
+ on the 4px/2px line. `thickness-small` is hard-coded to 2px in
235
+ alert-frame.css (it does NOT use the themed stroke-width token), so use a
236
+ literal 2px here; `thickness-large` is handled below. */
237
+ --_obc-readout-value-frame-stroke: 2px;
238
+ position: absolute;
239
+ inset-inline: calc(
240
+ -1 *
241
+ (
242
+ var(--_obc-readout-value-frame-padding-horizontal) -
243
+ var(--_obc-readout-value-frame-stroke) / 2
244
+ )
245
+ );
246
+ inset-block: calc(
247
+ -1 *
248
+ (
249
+ var(--_obc-readout-value-frame-padding-vertical) -
250
+ var(--_obc-readout-value-frame-stroke) / 2
251
+ )
252
+ );
253
+ /* Display-only: never intercept clicks on a clickable row. */
254
+ pointer-events: none;
255
+ }
256
+
257
+ /* thickness=large: obc-alert-frame's large outline uses the alert-frame border
258
+ weight token, so the overlay's outward offset must use the same width to keep
259
+ the stroke centred on the padding line. */
260
+ .value-alert-overlay.thickness-large {
261
+ --_obc-readout-value-frame-stroke: var(
262
+ --global-size-spacing-border-weight-alertframe
263
+ );
264
+ }
265
+
192
266
  /* The readout building blocks (advice / setpoint / value) now render inside
193
267
  `obc-readout-block`; their layout / colour / hinted-zero / data-quality CSS
194
268
  lives there. The list-item only bridges the public colour vars (see top) and
@@ -1262,7 +1262,7 @@ export const PerBlockState: Story = {
1262
1262
  value: {
1263
1263
  alert: {
1264
1264
  status: AlertType.Warning,
1265
- mode: ObcAlertFrameMode.unackedActive,
1265
+ mode: ObcAlertFrameMode.ackedActive,
1266
1266
  type: ObcAlertFrameType.Regular,
1267
1267
  },
1268
1268
  },
@@ -1296,7 +1296,7 @@ export const PerBlockState: Story = {
1296
1296
  value: {
1297
1297
  alert: {
1298
1298
  status: AlertType.Warning,
1299
- mode: ObcAlertFrameMode.unackedActive,
1299
+ mode: ObcAlertFrameMode.ackedActive,
1300
1300
  type: ObcAlertFrameType.Regular,
1301
1301
  },
1302
1302
  },
@@ -1322,7 +1322,7 @@ export const PerBlockState: Story = {
1322
1322
  dataQuality: ReadoutListItemDataQuality.lowIntegrity,
1323
1323
  alert: {
1324
1324
  status: AlertType.Warning,
1325
- mode: ObcAlertFrameMode.unackedActive,
1325
+ mode: ObcAlertFrameMode.ackedActive,
1326
1326
  type: ObcAlertFrameType.SmallSideFlip,
1327
1327
  showAlertCategoryIcon: true,
1328
1328
  },
@@ -1337,7 +1337,7 @@ export const PerBlockState: Story = {
1337
1337
  dataQuality: ReadoutListItemDataQuality.invalid,
1338
1338
  alert: {
1339
1339
  status: AlertType.Alarm,
1340
- mode: ObcAlertFrameMode.unackedActive,
1340
+ mode: ObcAlertFrameMode.ackedActive,
1341
1341
  type: ObcAlertFrameType.LargeSideFlip,
1342
1342
  showAlertCategoryIcon: true,
1343
1343
  },
@@ -1396,6 +1396,94 @@ export const Alarm: Story = {
1396
1396
  }),
1397
1397
  };
1398
1398
 
1399
+ // The value alert frame now wraps the whole reading — value + degree + unit — and
1400
+ // is drawn as a pure overlay (4px/2px padding, stroke centred on that line), so
1401
+ // toggling it changes neither the row height nor the value/unit column positions.
1402
+ // Each combo is shown unframed then framed so the constant geometry is obvious;
1403
+ // the per-item outline marks the layout box (which the frame never grows).
1404
+ const VALUE_FRAME: AlertFrameConfig = {
1405
+ status: AlertType.Warning,
1406
+ mode: ObcAlertFrameMode.ackedActive,
1407
+ type: ObcAlertFrameType.Regular,
1408
+ };
1409
+
1410
+ const VALUE_FRAME_COMBOS: {label: string; config: ReadoutItemConfig}[] = [
1411
+ {label: 'value + unit', config: {value: 123, unit: 'kn'}},
1412
+ {
1413
+ label: 'value + degree + unit',
1414
+ config: {value: 287, unit: 'T', options: {hasDegree: true}},
1415
+ },
1416
+ {
1417
+ label: 'value + unit + setpoint',
1418
+ config: {value: 123, unit: 'kn', hasSetpoint: true, setpoint: 120},
1419
+ },
1420
+ {
1421
+ label: 'value + unit + advice + setpoint',
1422
+ config: {
1423
+ value: 123,
1424
+ unit: 'kn',
1425
+ hasSetpoint: true,
1426
+ setpoint: 120,
1427
+ hasAdvice: true,
1428
+ advice: 118,
1429
+ },
1430
+ },
1431
+ ];
1432
+
1433
+ export const ValueAlertFrame: Story = {
1434
+ render: () => html`
1435
+ <style>
1436
+ .rli-vf {
1437
+ display: flex;
1438
+ flex-direction: column;
1439
+ gap: 12px;
1440
+ width: 380px;
1441
+ }
1442
+ .rli-vf-row {
1443
+ display: flex;
1444
+ align-items: center;
1445
+ gap: 12px;
1446
+ }
1447
+ .rli-vf-tag {
1448
+ width: 180px;
1449
+ flex: none;
1450
+ font: 10px/1.2 var(--global-typography-ui-label-font-family, sans-serif);
1451
+ text-transform: uppercase;
1452
+ letter-spacing: 0.06em;
1453
+ color: var(--element-neutral-color, #777);
1454
+ }
1455
+ .rli-vf-item {
1456
+ flex: 1;
1457
+ outline: 1px solid rgba(0, 0, 0, 0.12);
1458
+ }
1459
+ </style>
1460
+ <div class="rli-vf">
1461
+ ${VALUE_FRAME_COMBOS.flatMap((combo) =>
1462
+ [false, true].map(
1463
+ (framed) => html`
1464
+ <div class="rli-vf-row">
1465
+ <div class="rli-vf-tag">
1466
+ ${combo.label} ${framed ? '· framed' : '· plain'}
1467
+ </div>
1468
+ <div class="rli-vf-item">
1469
+ ${renderItem({
1470
+ ...combo.config,
1471
+ label: 'Heading',
1472
+ options: {
1473
+ ...combo.config.options,
1474
+ size: ReadoutListItemSize.medium,
1475
+ value: framed ? {alert: VALUE_FRAME} : undefined,
1476
+ },
1477
+ })}
1478
+ </div>
1479
+ </div>
1480
+ `
1481
+ )
1482
+ )}
1483
+ </div>
1484
+ `,
1485
+ };
1486
+
1399
1487
  // Each row drops a different part; the per-row outline makes it obvious that the
1400
1488
  // row height and the bottom baseline stay constant regardless of what's missing.
1401
1489
  const MISSING_PARTS_ROWS: {title: string; config: ReadoutItemConfig}[] = [
@@ -1500,9 +1588,11 @@ export const MissingParts: Story = {
1500
1588
  * value digit edges, not both. Aligning the value edges would need either a
1501
1589
  * constant degree reserve (which widens the smaller rows' `°` — visually
1502
1590
  * undesirable) or pinning the value edge and letting the unit column stagger;
1503
- * left for the designer. Two rows also carry per-block (nested) state — a value
1504
- * `invalid` chip and a value alert frame to confirm per-block state does not
1505
- * shift the value edge.
1591
+ * left for the designer. Several rows also carry per-block / row state — a value
1592
+ * `invalid` chip (Battery), a value alert frame (Fuel), a `low-integrity` advice
1593
+ * chip (Pressure), and a whole-row `low-integrity` + `invalid` setpoint + a
1594
+ * row-level alert frame with a badge-icon flap (Distance) — to confirm none of
1595
+ * them shift the aligned columns.
1506
1596
  */
1507
1597
  type AlignmentRow = {
1508
1598
  label: string;
@@ -1523,6 +1613,9 @@ type AlignmentRow = {
1523
1613
  hasValueIcon?: boolean;
1524
1614
  valueDataQuality?: ReadoutListItemDataQuality;
1525
1615
  valueAlert?: AlertFrameConfig;
1616
+ setpointDataQuality?: ReadoutListItemDataQuality;
1617
+ adviceDataQuality?: ReadoutListItemDataQuality;
1618
+ alert?: AlertFrameConfig;
1526
1619
  };
1527
1620
 
1528
1621
  const ALIGNMENT_ROWS: AlignmentRow[] = [
@@ -1548,6 +1641,8 @@ const ALIGNMENT_ROWS: AlignmentRow[] = [
1548
1641
  setpoint: 1015,
1549
1642
  hasAdvice: true,
1550
1643
  advice: 1008,
1644
+ // per-block advice low-integrity — the advice chip must not shift the columns
1645
+ adviceDataQuality: ReadoutListItemDataQuality.lowIntegrity,
1551
1646
  },
1552
1647
  // negative value + fraction + low-integrity data quality
1553
1648
  {
@@ -1557,7 +1652,10 @@ const ALIGNMENT_ROWS: AlignmentRow[] = [
1557
1652
  fractionDigits: 1,
1558
1653
  dataQuality: ReadoutListItemDataQuality.lowIntegrity,
1559
1654
  },
1560
- // long unit + advice + setpoint (the "miles" row)
1655
+ // long unit + advice + setpoint (the "miles" row) — also a "kitchen sink of
1656
+ // state": whole row low-integrity, an invalid setpoint chip, and a row-level
1657
+ // alert frame with a badge-icon flap, all at once, to prove none of them shift
1658
+ // the aligned columns.
1561
1659
  {
1562
1660
  label: 'Distance',
1563
1661
  value: 4.2,
@@ -1567,6 +1665,14 @@ const ALIGNMENT_ROWS: AlignmentRow[] = [
1567
1665
  setpoint: 4.5,
1568
1666
  hasAdvice: true,
1569
1667
  advice: 4,
1668
+ dataQuality: ReadoutListItemDataQuality.lowIntegrity,
1669
+ setpointDataQuality: ReadoutListItemDataQuality.invalid,
1670
+ alert: {
1671
+ status: AlertType.Warning,
1672
+ mode: ObcAlertFrameMode.ackedActive,
1673
+ type: ObcAlertFrameType.SmallSideFlip,
1674
+ showAlertCategoryIcon: true,
1675
+ },
1570
1676
  },
1571
1677
  // kitchen sink: value + setpoint + advice + leading icon + value icon
1572
1678
  {
@@ -1612,7 +1718,7 @@ const ALIGNMENT_ROWS: AlignmentRow[] = [
1612
1718
  unit: '%',
1613
1719
  valueAlert: {
1614
1720
  status: AlertType.Warning,
1615
- mode: ObcAlertFrameMode.unackedActive,
1721
+ mode: ObcAlertFrameMode.ackedActive,
1616
1722
  type: ObcAlertFrameType.Regular,
1617
1723
  },
1618
1724
  },
@@ -1651,7 +1757,7 @@ const alignmentStyle = `
1651
1757
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--element-neutral-color, #777);
1652
1758
  }
1653
1759
  .rli-align-col {
1654
- display: flex; flex-direction: column; gap: 2px; width: 480px;
1760
+ display: flex; flex-direction: column; gap: 8px; width: 480px;
1655
1761
  padding: 8px; border: 1px dashed var(--border-divider-color, #ccc); border-radius: 8px;
1656
1762
  }
1657
1763
  `;
@@ -1678,6 +1784,7 @@ function renderAlignmentColumn(aligned: boolean, showDebugOverlay: boolean) {
1678
1784
  fractionDigits: row.fractionDigits ?? 0,
1679
1785
  priority: row.priority,
1680
1786
  dataQuality: row.dataQuality,
1787
+ alert: row.alert,
1681
1788
  hasLeadingIcon: row.hasLeadingIcon,
1682
1789
  value: {
1683
1790
  hasIcon: row.hasValueIcon,
@@ -1689,6 +1796,16 @@ function renderAlignmentColumn(aligned: boolean, showDebugOverlay: boolean) {
1689
1796
  // independent of each row's own fractionDigits.
1690
1797
  ...(aligned ? {spaceReserver: VALUE_RESERVER} : {}),
1691
1798
  },
1799
+ // Setpoint / advice carry their per-block data quality in both
1800
+ // columns; the shared reserver is added only in the aligned column.
1801
+ setpoint: {
1802
+ dataQuality: row.setpointDataQuality,
1803
+ ...(aligned ? {spaceReserver: VALUE_RESERVER} : {}),
1804
+ },
1805
+ advice: {
1806
+ dataQuality: row.adviceDataQuality,
1807
+ ...(aligned ? {spaceReserver: VALUE_RESERVER} : {}),
1808
+ },
1692
1809
  ...(aligned
1693
1810
  ? {
1694
1811
  // Non-degree rows reserve the degree column so their digits
@@ -1696,8 +1813,6 @@ function renderAlignmentColumn(aligned: boolean, showDebugOverlay: boolean) {
1696
1813
  hasDegreeSpacer: !(row.hasDegree ?? false),
1697
1814
  maxDigits: MAX_INTEGER_DIGITS,
1698
1815
  unit: {spaceReserver: LONGEST_UNIT},
1699
- setpoint: {spaceReserver: VALUE_RESERVER},
1700
- advice: {spaceReserver: VALUE_RESERVER},
1701
1816
  }
1702
1817
  : {}),
1703
1818
  },
@@ -22,7 +22,11 @@ import {
22
22
  import {
23
23
  type AlertFrameConfig,
24
24
  wrapWithAlertFrame,
25
+ ObcAlertFrameType,
26
+ ObcAlertFrameThickness,
27
+ ObcAlertFrameMode,
25
28
  } from '../../components/alert-frame/alert-frame.js';
29
+ import {AlertType} from '../../types.js';
26
30
 
27
31
  // The value weight maps straight to obc-textbox's font weights (regular /
28
32
  // semibold / bold). Re-exported so consumers can set `valueOptions.weight`
@@ -96,6 +100,14 @@ export interface ReadoutBlockState {
96
100
  alert?: false | AlertFrameConfig;
97
101
  }
98
102
 
103
+ /**
104
+ * Per-value options. Unlike setpoint/advice, the value's `alert` frame AND
105
+ * `dataQuality` chip wrap the whole reading — value (+ value-icon) + degree +
106
+ * trailing unit — rather than the value alone. The alert frame is a pure overlay
107
+ * (4px/2px padding, stroke centred on that line); the data-quality chip reuses
108
+ * the block chip's `outline` styling with no extra padding. Neither shifts
109
+ * content or changes the row height / column alignment (Figma 58:10120).
110
+ */
99
111
  export interface ReadoutValueOptions extends ReadoutBlockState {
100
112
  /** Render the unfilled leading positions as muted zeroes (requires `maxDigits`). */
101
113
  hintedZeros?: boolean;
@@ -661,6 +673,34 @@ export class ObcReadoutListItem extends LitElement {
661
673
  alert: this.setpointOptions?.alert,
662
674
  })
663
675
  : nothing}
676
+ ${this.renderValueReading()}
677
+ </div>
678
+ `;
679
+ }
680
+
681
+ /**
682
+ * The value reading: the value block, its degree column and the trailing unit
683
+ * grouped in one relatively-positioned wrapper so the value alert frame AND the
684
+ * value data-quality chip can wrap value + degree + unit together (Figma
685
+ * 58:10120).
686
+ *
687
+ * Moving the degree / unit into this wrapper (the new last child of
688
+ * `.value-cluster`) preserves every existing gap, so the underlying content
689
+ * stays column-aligned with or without the frame / chip. Both the value alert
690
+ * (overlay) and the value data-quality chip are applied here — NOT inside
691
+ * `obc-readout-block` — so they extend over the unit; setpoint/advice keep their
692
+ * own block-level frame and chip. The chip uses `outline` (not `border`), so it
693
+ * never shifts the value's width / column alignment.
694
+ */
695
+ private renderValueReading(): TemplateResult {
696
+ return html`
697
+ <div
698
+ class=${classMap({
699
+ 'value-reading': true,
700
+ ...this.dataQualityClasses(this.valueOptions?.dataQuality),
701
+ })}
702
+ part="value-reading"
703
+ >
664
704
  ${this.hasValue
665
705
  ? this.renderBlock({
666
706
  variant: ReadoutBlockVariant.value,
@@ -672,10 +712,52 @@ export class ObcReadoutListItem extends LitElement {
672
712
  spaceReserver: this.valueOptions?.spaceReserver,
673
713
  off: this.off,
674
714
  hasIcon: this.valueOptions?.hasIcon ?? false,
675
- dataQuality: this.valueOptions?.dataQuality,
676
- alert: this.valueOptions?.alert,
677
715
  })
678
716
  : nothing}
717
+ ${this.renderValueUnitGap()}
718
+ <div class="unit-area" part="unit-area">
719
+ ${this.renderTrailingUnit()} ${this.renderDegreeSpacer()}
720
+ </div>
721
+ ${this.renderValueAlertOverlay()}
722
+ </div>
723
+ `;
724
+ }
725
+
726
+ /**
727
+ * The value alert frame, drawn as a pure overlay around the value reading
728
+ * (value + degree + unit). It reserves no space — the `obc-alert-frame` sits in
729
+ * an absolutely-positioned box offset outward (see `.value-alert-overlay` in
730
+ * the CSS) so its stroke is centred on the 4px/2px padding line and toggling it
731
+ * never shifts content or row height. Renders only when `valueOptions.alert` is
732
+ * a config object.
733
+ */
734
+ private renderValueAlertOverlay(): TemplateResult | typeof nothing {
735
+ const alert = this.valueOptions?.alert;
736
+ if (typeof alert !== 'object' || alert === null) {
737
+ return nothing;
738
+ }
739
+ const thickness = alert.thickness ?? ObcAlertFrameThickness.Small;
740
+ return html`
741
+ <div
742
+ class=${classMap({
743
+ 'value-alert-overlay': true,
744
+ // The outward offset is thickness-dependent (see the CSS): large frames
745
+ // draw a wider outline, so the box must sit further out to stay centred.
746
+ 'thickness-large': thickness === ObcAlertFrameThickness.Large,
747
+ })}
748
+ aria-hidden="true"
749
+ >
750
+ <obc-alert-frame
751
+ part="value-alert-frame"
752
+ .type=${alert.type ?? ObcAlertFrameType.Regular}
753
+ .thickness=${thickness}
754
+ .status=${alert.status ?? AlertType.Alarm}
755
+ .mode=${alert.mode ?? ObcAlertFrameMode.ackedActive}
756
+ .showIcon=${alert.showIcon ?? false}
757
+ .showAlertCategoryIcon=${alert.showAlertCategoryIcon ?? true}
758
+ .wrapContent=${false}
759
+ .fullWidth=${false}
760
+ ></obc-alert-frame>
679
761
  </div>
680
762
  `;
681
763
  }
@@ -753,10 +835,7 @@ export class ObcReadoutListItem extends LitElement {
753
835
  <div class="content" part="content">
754
836
  ${this.renderLabelContainer()}
755
837
  <div class="value-area" part="value-area">
756
- ${this.renderValueCluster()} ${this.renderValueUnitGap()}
757
- <div class="unit-area" part="unit-area">
758
- ${this.renderTrailingUnit()} ${this.renderDegreeSpacer()}
759
- </div>
838
+ ${this.renderValueCluster()}
760
839
  </div>
761
840
  ${this.renderTrailingSource()}
762
841
  </div>