@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.152 → 2.0.0-next.153

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 (90) hide show
  1. package/.storybook/vitest.setup.ts +1 -1
  2. package/bundle/openbridge-webcomponents.bundle.js +4717 -4485
  3. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  4. package/custom-elements.json +374 -52
  5. package/dist/alert-severity.d.ts +19 -1
  6. package/dist/alert-severity.d.ts.map +1 -1
  7. package/dist/alert-severity.js +48 -2
  8. package/dist/alert-severity.js.map +1 -1
  9. package/dist/components/alert-button/alert-button.css.js +7 -1513
  10. package/dist/components/alert-button/alert-button.css.js.map +1 -1
  11. package/dist/components/alert-button/alert-button.d.ts +40 -96
  12. package/dist/components/alert-button/alert-button.d.ts.map +1 -1
  13. package/dist/components/alert-button/alert-button.js +40 -107
  14. package/dist/components/alert-button/alert-button.js.map +1 -1
  15. package/dist/components/alert-button-item/alert-button-item.css.js +1570 -0
  16. package/dist/components/alert-button-item/alert-button-item.css.js.map +1 -0
  17. package/dist/components/alert-button-item/alert-button-item.d.ts +134 -0
  18. package/dist/components/alert-button-item/alert-button-item.d.ts.map +1 -0
  19. package/dist/components/alert-button-item/alert-button-item.js +210 -0
  20. package/dist/components/alert-button-item/alert-button-item.js.map +1 -0
  21. package/dist/components/alert-counter-item/alert-counter-item.css.js +25 -0
  22. package/dist/components/alert-counter-item/alert-counter-item.css.js.map +1 -0
  23. package/dist/components/alert-counter-item/alert-counter-item.d.ts +77 -0
  24. package/dist/components/alert-counter-item/alert-counter-item.d.ts.map +1 -0
  25. package/dist/components/alert-counter-item/alert-counter-item.js +87 -0
  26. package/dist/components/alert-counter-item/alert-counter-item.js.map +1 -0
  27. package/dist/components/alert-frame/alert-frame.css.js +23 -27
  28. package/dist/components/alert-frame/alert-frame.css.js.map +1 -1
  29. package/dist/components/alert-frame/alert-frame.d.ts +7 -6
  30. package/dist/components/alert-frame/alert-frame.d.ts.map +1 -1
  31. package/dist/components/alert-frame/alert-frame.js +6 -9
  32. package/dist/components/alert-frame/alert-frame.js.map +1 -1
  33. package/dist/components/tree-navigation-item/tree-navigation-item.d.ts +7 -34
  34. package/dist/components/tree-navigation-item/tree-navigation-item.d.ts.map +1 -1
  35. package/dist/components/tree-navigation-item/tree-navigation-item.js +3 -32
  36. package/dist/components/tree-navigation-item/tree-navigation-item.js.map +1 -1
  37. package/dist/generated/locales/es-419.d.ts +11 -3
  38. package/dist/generated/locales/es-419.d.ts.map +1 -1
  39. package/dist/generated/locales/es-419.js +11 -3
  40. package/dist/generated/locales/es-419.js.map +1 -1
  41. package/dist/generated/locales/fi-FI.d.ts +8 -0
  42. package/dist/generated/locales/fi-FI.d.ts.map +1 -1
  43. package/dist/generated/locales/fi-FI.js +8 -0
  44. package/dist/generated/locales/fi-FI.js.map +1 -1
  45. package/dist/navigation-instruments/readout/readout.css.js +4 -29
  46. package/dist/navigation-instruments/readout/readout.css.js.map +1 -1
  47. package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
  48. package/dist/navigation-instruments/readout/readout.js +1 -9
  49. package/dist/navigation-instruments/readout/readout.js.map +1 -1
  50. package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js +5 -34
  51. package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js.map +1 -1
  52. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
  53. package/dist/navigation-instruments/readout-list-item/readout-list-item.js +1 -9
  54. package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
  55. package/dist/palettes/blinking.d.ts +6 -2
  56. package/dist/palettes/blinking.d.ts.map +1 -1
  57. package/dist/palettes/blinking.js +6 -2
  58. package/dist/palettes/blinking.js.map +1 -1
  59. package/dist/types.d.ts +15 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/types.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/alert-severity.spec.ts +81 -2
  64. package/src/alert-severity.ts +74 -0
  65. package/src/components/alert-button/alert-button.css +7 -196
  66. package/src/components/alert-button/alert-button.spec.ts +119 -68
  67. package/src/components/alert-button/alert-button.stories.ts +24 -0
  68. package/src/components/alert-button/alert-button.ts +49 -211
  69. package/src/components/alert-button-item/alert-button-item.css +242 -0
  70. package/src/components/alert-button-item/alert-button-item.spec.ts +299 -0
  71. package/src/components/alert-button-item/alert-button-item.stories.ts +178 -0
  72. package/src/components/alert-button-item/alert-button-item.ts +289 -0
  73. package/src/components/alert-counter-item/alert-counter-item.css +15 -0
  74. package/src/components/alert-counter-item/alert-counter-item.spec.ts +91 -0
  75. package/src/components/alert-counter-item/alert-counter-item.stories.ts +66 -0
  76. package/src/components/alert-counter-item/alert-counter-item.ts +128 -0
  77. package/src/components/alert-frame/alert-frame.css +23 -27
  78. package/src/components/alert-frame/alert-frame.spec.ts +19 -1
  79. package/src/components/alert-frame/alert-frame.stories.ts +47 -0
  80. package/src/components/alert-frame/alert-frame.ts +16 -17
  81. package/src/components/tree-navigation-item/tree-navigation-item.ts +10 -60
  82. package/src/generated/locales/es-419.ts +11 -3
  83. package/src/generated/locales/fi-FI.ts +8 -0
  84. package/src/navigation-instruments/readout/readout.css +4 -29
  85. package/src/navigation-instruments/readout/readout.ts +1 -9
  86. package/src/navigation-instruments/readout-list-item/readout-list-item.css +5 -33
  87. package/src/navigation-instruments/readout-list-item/readout-list-item.ts +1 -9
  88. package/src/palettes/blinking.spec.ts +38 -23
  89. package/src/palettes/blinking.ts +12 -4
  90. package/src/types.ts +16 -0
@@ -24,12 +24,12 @@
24
24
  position: relative;
25
25
  width: 100%;
26
26
  height: 100%;
27
- border-radius: calc(
28
- var(--ui-components-button-border-radius) + var(--thickness) / 2
29
- );
27
+ /* The stroke's centreline follows this radius, like a Figma centre stroke. */
28
+ border-radius: var(--ui-components-button-border-radius);
30
29
  outline: var(--thickness) solid var(--bg-color);
31
- outline-offset: 0;
32
- /* +2 px outward while on, both thicknesses (Figma Flash=yes: 2 → 4 px). */
30
+ /* Centre stroke: frames on touching components share one edge. */
31
+ outline-offset: calc(-1 * var(--thickness) / 2);
32
+ /* +2 px while on, 1 px each side, both thicknesses (Figma Flash=yes: 2 → 4 px). */
33
33
  --blink-width: 2px;
34
34
  &.thickness-small {
35
35
  --thickness: 2px;
@@ -62,7 +62,9 @@
62
62
  --flash-transition: 50ms linear;
63
63
 
64
64
  &.flash-effect-outline-eased {
65
- transition: outline-width var(--flash-transition);
65
+ transition:
66
+ outline-width var(--flash-transition),
67
+ outline-offset var(--flash-transition);
66
68
 
67
69
  .flap {
68
70
  transition: box-shadow var(--flash-transition);
@@ -88,14 +90,10 @@
88
90
  &.unacked-active,
89
91
  &.unacked-rectified {
90
92
  outline-width: calc(var(--thickness) + var(--blink-width-dynamic));
91
- }
92
-
93
- &.unacked-active {
94
- --flap-grow: var(--blink-width-dynamic);
95
- }
96
-
97
- &.unacked-rectified {
98
- /* The dashed stroke is centred, so it moves out by half its growth. */
93
+ outline-offset: calc(
94
+ -1 * (var(--thickness) + var(--blink-width-dynamic)) / 2
95
+ );
96
+ /* The stroke grows on both sides, so its outer edge moves by half. */
99
97
  --flap-grow: calc(var(--blink-width-dynamic) / 2);
100
98
  }
101
99
 
@@ -160,9 +158,9 @@
160
158
  gap: 2px;
161
159
 
162
160
  &.small {
163
- top: calc(-1 * var(--thickness));
161
+ top: calc(-1 * var(--thickness) / 2);
164
162
  right: calc(
165
- -1 * var(--app-components-alert-frame-icon-size) - var(--thickness) / 2 -
163
+ -1 * var(--app-components-alert-frame-icon-size) -
166
164
  var(--ui-components-badge-padding) * 2
167
165
  );
168
166
  }
@@ -272,10 +270,10 @@
272
270
 
273
271
  &.large {
274
272
  flex-direction: column;
275
- top: calc(-1 * var(--thickness));
276
- bottom: calc(-1 * var(--thickness));
273
+ top: calc(-1 * var(--thickness) / 2);
274
+ bottom: calc(-1 * var(--thickness) / 2);
277
275
  right: calc(
278
- -1 * var(--app-components-alert-frame-icon-size) + var(--thickness) / 2 -
276
+ -1 * var(--app-components-alert-frame-icon-size) + var(--thickness) -
279
277
  var(--ui-components-badge-padding) * 2
280
278
  );
281
279
  border-radius: 0px
@@ -285,13 +283,12 @@
285
283
 
286
284
  &.bottom,
287
285
  &.top {
288
- left: calc(-1 * var(--thickness));
289
- right: calc(-1 * var(--thickness));
286
+ left: calc(-1 * var(--thickness) / 2);
287
+ right: calc(-1 * var(--thickness) / 2);
290
288
  padding: var(--app-components-alert-frame-large-flip-padding-vertical)
291
289
  var(--app-components-alert-frame-large-flip-4px-padding-horizontal);
292
290
  bottom: calc(
293
- -1 * var(--thickness) / 2 -
294
- var(--global-typography-ui-label-line-height) -
291
+ -1 * var(--global-typography-ui-label-line-height) -
295
292
  var(--app-components-alert-frame-large-flip-padding-vertical) * 2
296
293
  );
297
294
  border-radius: 0px 0px var(--button-border-radius-bottom-right)
@@ -308,7 +305,7 @@
308
305
  .text-size-large & {
309
306
  @mixin font-body;
310
307
  bottom: calc(
311
- var(--thickness) / 2 - var(--global-typography-ui-body-line-height) -
308
+ var(--thickness) - var(--global-typography-ui-body-line-height) -
312
309
  var(--app-components-alert-frame-large-flip-padding-vertical) * 2
313
310
  );
314
311
 
@@ -321,8 +318,7 @@
321
318
  &.top {
322
319
  bottom: unset !important;
323
320
  top: calc(
324
- -1 * var(--thickness) / 2 -
325
- var(--global-typography-ui-label-line-height) -
321
+ -1 * var(--global-typography-ui-label-line-height) -
326
322
  var(--app-components-alert-frame-large-flip-padding-vertical) * 2
327
323
  );
328
324
  border-radius: var(--button-border-radius-bottom-right)
@@ -330,7 +326,7 @@
330
326
 
331
327
  .text-size-large & {
332
328
  top: calc(
333
- var(--thickness) / 2 - var(--global-typography-ui-body-line-height) -
329
+ var(--thickness) - var(--global-typography-ui-body-line-height) -
334
330
  var(--app-components-alert-frame-large-flip-padding-vertical) * 2
335
331
  );
336
332
  }
@@ -221,6 +221,21 @@ describe('obc-alert-frame flashing lifecycle', () => {
221
221
  expect(midway).toBeLessThan(4);
222
222
  });
223
223
 
224
+ it('centres the stroke on the frame edge and grows it 1 px each side while on', async () => {
225
+ const el = await setup(ObcAlertFrameMode.unackedActive);
226
+ const wrapper = el.shadowRoot!.querySelector('.wrapper') as HTMLElement;
227
+ const outline = () => {
228
+ const style = getComputedStyle(wrapper);
229
+ return [style.outlineWidth, style.outlineOffset];
230
+ };
231
+ const [flash] = el.getAnimations();
232
+ flash.pause();
233
+ flash.currentTime = 100;
234
+ expect(outline()).toEqual(['4px', '-2px']);
235
+ flash.currentTime = 500;
236
+ expect(outline()).toEqual(['2px', '-1px']);
237
+ });
238
+
224
239
  describe('rectified dash', () => {
225
240
  it('draws the 12/6 dash as one svg path whose stroke width flashes', async () => {
226
241
  const el = await setup(ObcAlertFrameMode.unackedRectified);
@@ -263,7 +278,10 @@ describe('obc-alert-frame flashing lifecycle', () => {
263
278
 
264
279
  const sharp = path().getAttribute('d')!;
265
280
  expect(sharp).not.toBe(rounded);
266
- expect(sharp.startsWith('M3 3 H')).toBe(true);
281
+ // The sharp corner sits on the wrapper corner: the svg is shifted by -pad.
282
+ expect(sharp.startsWith('M2 2 H')).toBe(true);
283
+ const svg = el.shadowRoot!.querySelector('svg.dash') as SVGSVGElement;
284
+ expect(svg.style.getPropertyValue('--dash-pad')).toBe('2px');
267
285
  });
268
286
 
269
287
  it('removes the svg when the mode leaves unacked-rectified', async () => {
@@ -290,6 +290,53 @@ export const CriticalUnacked: Story = {
290
290
  },
291
291
  };
292
292
 
293
+ const ADJACENT_FRAMES: {
294
+ status: AlertType;
295
+ mode: ObcAlertFrameMode;
296
+ thickness: ObcAlertFrameThickness;
297
+ }[] = [
298
+ {
299
+ status: AlertType.Warning,
300
+ mode: ObcAlertFrameMode.unackedActive,
301
+ thickness: ObcAlertFrameThickness.Small,
302
+ },
303
+ {
304
+ status: AlertType.Alarm,
305
+ mode: ObcAlertFrameMode.ackedActive,
306
+ thickness: ObcAlertFrameThickness.Small,
307
+ },
308
+ {
309
+ status: AlertType.Caution,
310
+ mode: ObcAlertFrameMode.ackedActive,
311
+ thickness: ObcAlertFrameThickness.Large,
312
+ },
313
+ ];
314
+
315
+ export const AdjacentFrames: Story = {
316
+ parameters: {
317
+ docs: {
318
+ description: {
319
+ story:
320
+ 'Frames on touching components share one edge: every stroke is centred on the edge of its component. The top frame is in its on phase, so its wider stroke shows on both sides of the thinner one below it.',
321
+ },
322
+ },
323
+ },
324
+ render: () =>
325
+ html`<div style="display: grid; width: 240px; padding: 16px">
326
+ ${ADJACENT_FRAMES.map(
327
+ (frame) =>
328
+ html`<div style="position: relative; height: 56px">
329
+ <obc-alert-frame
330
+ .type=${ObcAlertFrameType.Regular}
331
+ .status=${frame.status}
332
+ .mode=${frame.mode}
333
+ .thickness=${frame.thickness}
334
+ ></obc-alert-frame>
335
+ </div>`
336
+ )}
337
+ </div>`,
338
+ };
339
+
293
340
  export const FlashComparison: Story = {
294
341
  tags: ['skip-test'],
295
342
  render: () => {
@@ -66,10 +66,10 @@ export enum ObcAlertFrameMode {
66
66
  }
67
67
 
68
68
  /**
69
- * How the frame renders the on phase of a flash: `outline` grows the outline
70
- * outward by 2 px in one step (the design); `outline-eased` grows it the same
71
- * way over a 50 ms transition. The eased variant exists for design evaluation
72
- * and may be removed (#1224).
69
+ * How the frame renders the on phase of a flash: `outline` grows the centred
70
+ * stroke by 2 px, 1 px on each side, in one step (the design); `outline-eased`
71
+ * grows it the same way over a 50 ms transition. The eased variant exists for
72
+ * design evaluation and may be removed (#1224).
73
73
  */
74
74
  export enum ObcAlertFrameFlashEffect {
75
75
  Outline = 'outline',
@@ -94,7 +94,7 @@ interface DashBox {
94
94
  radii: RoundedRect['radii'];
95
95
  }
96
96
 
97
- /** Room the svg leaves around the box for the stroke's on-phase growth. */
97
+ /** On-phase stroke growth; the svg leaves half of stroke plus growth around the box. */
98
98
  const DASH_FLASH_GROWTH_PX = 2;
99
99
 
100
100
  function sameDashBox(a: DashBox, b: DashBox | undefined): boolean {
@@ -130,6 +130,7 @@ export interface AlertFrameConfig {
130
130
  * - `large-side-flip`: Adds a larger, vertical side flap with a status icon and optional custom icon.
131
131
  * - `bottom-flip`: Adds a bottom flap with a status icon, label, and timer slots.
132
132
  * - **Thickness options:** Choose between `small` (thin border) and `large` (thick border) for visual emphasis.
133
+ * - **Centred stroke:** The stroke straddles the frame edge, half over the content and half outside, so frames on touching components share one edge. While flashing it grows by 1 px on each side.
133
134
  * - **Status indication:** Displays different color schemes and icons for the legacy statuses (`alarm`, `warning`, `caution`) and the level statuses (`level-critical`, `level-high`, `level-medium`, `level-low`, `level-diagnostic`).
134
135
  * - **Acknowledgement mode:** The `mode` property reflects the alert lifecycle state — `acked-active` (default, steady), `unacked-active` (flashes) and `unacked-rectified` (dashed, flashes) — and `flashingSpeed` picks the tempo.
135
136
  * - **Content wrapping:** When `wrapContent` is true, the frame wraps and sizes itself to its slotted content rather than overlaying a fixed region.
@@ -202,7 +203,7 @@ export interface AlertFrameConfig {
202
203
  * (critical/alarm/high fast, warning/medium slow, low very slow, every rectified alert very
203
204
  * slow, caution and diagnostic fixed), `fast`, `slow`, `very-slow` force a tempo, `fixed`
204
205
  * never flashes. Acknowledged frames are always steady.
205
- * @property flashEffect - `outline` (default) grows the outline by 2 px while on in one step;
206
+ * @property flashEffect - `outline` (default) grows the outline by 2 px, 1 px each side, while on in one step;
206
207
  * `outline-eased` grows it over a 50 ms transition, a design-evaluation option.
207
208
  * @slot - Default slot for main alert content.
208
209
  * @slot icon - Custom icon for the flap (large-side-flip, bottom-flip).
@@ -327,18 +328,16 @@ export class ObcAlertFrame extends LitElement {
327
328
  if (this.mode !== ObcAlertFrameMode.unackedRectified || !box) {
328
329
  return nothing;
329
330
  }
330
- const pad = box.thickness + DASH_FLASH_GROWTH_PX;
331
- // A second, wider path for the on phase would have longer corner arcs
332
- // and its dashes would drift around the frame.
331
+ const pad = (box.thickness + DASH_FLASH_GROWTH_PX) / 2;
332
+ // The centreline is the wrapper edge, as for the solid outline. A second,
333
+ // wider path for the on phase would have longer corner arcs and its
334
+ // dashes would drift around the frame.
333
335
  const centreline: RoundedRect = {
334
- x: pad - box.thickness / 2,
335
- y: pad - box.thickness / 2,
336
- width: box.width + box.thickness,
337
- height: box.height + box.thickness,
338
- // A radius of 0 is a sharp edge and must stay square.
339
- radii: box.radii.map((r) =>
340
- r > 0 ? r + box.thickness / 2 : 0
341
- ) as RoundedRect['radii'],
336
+ x: pad,
337
+ y: pad,
338
+ width: box.width,
339
+ height: box.height,
340
+ radii: box.radii,
342
341
  };
343
342
  const width = box.width + 2 * pad;
344
343
  const height = box.height + 2 * pad;
@@ -8,7 +8,8 @@ import '../../icons/icon-chevron-right-google.js';
8
8
  import '../../icons/icon-alert-header-aggregated-iec.js';
9
9
  import '../../icons/icon-alert-header-group-iec.js';
10
10
  import '../badge/badge.js';
11
- import {AlertType, ALERT_SEVERITY_PRIORITY} from '../../types.js';
11
+ import type {AlertCounts} from '../../types.js';
12
+ import {rankAlertCounts, type RankedAlertCount} from '../../alert-severity.js';
12
13
 
13
14
  /**
14
15
  * Guide line drawn for one indentation column. Normally computed by
@@ -43,36 +44,16 @@ export enum TreeTerminalType {
43
44
  }
44
45
 
45
46
  /**
46
- * Per-severity alert counts for a tree row's trailing badge(s). Each `count*`
47
- * is the number of active alerts of that severity at or beneath the row the
48
- * level severities `level-critical`, `level-high`, `level-medium`, `level-low`,
49
- * and `level-diagnostic`, plus the IEC severities `alarm`, `warning`, and
50
- * `caution`. Badges are ordered and aggregated by the shared
51
- * `ALERT_SEVERITY_PRIORITY` ranking, most to least severe.
47
+ * Per-severity alert counts for a tree row's trailing badge(s): the active
48
+ * alerts at or beneath the row, ordered by `rankAlertCounts`.
52
49
  *
53
50
  * Set `combine` to collapse the counts into a single badge showing the total,
54
51
  * styled as the highest category present; otherwise one badge is rendered per
55
52
  * non-zero count.
56
53
  */
57
- export interface TreeNavigationItemAlerts {
54
+ export interface TreeNavigationItemAlerts extends AlertCounts {
58
55
  /** Collapse all counts into one badge: total count, highest-severity style. */
59
56
  combine?: boolean;
60
- /** Number of level-critical alerts. */
61
- countLevelCritical?: number;
62
- /** Number of alarm alerts. */
63
- countAlarm?: number;
64
- /** Number of level-high alerts. */
65
- countLevelHigh?: number;
66
- /** Number of warning alerts. */
67
- countWarning?: number;
68
- /** Number of level-medium alerts. */
69
- countLevelMedium?: number;
70
- /** Number of caution alerts. */
71
- countCaution?: number;
72
- /** Number of level-low alerts. */
73
- countLevelLow?: number;
74
- /** Number of level-diagnostic alerts. */
75
- countLevelDiagnostic?: number;
76
57
  }
77
58
 
78
59
  /**
@@ -177,8 +158,8 @@ export class ObcTreeNavigationItem extends LitElement {
177
158
  * every count at 0) for a row with no alerts. See {@link TreeNavigationItemAlerts}.
178
159
  *
179
160
  * - When `combine` is true, a single badge is shown: its number is the sum
180
- * of all counts and its severity is the highest category present
181
- * (critical → alarm → warning → caution).
161
+ * of all counts and its severity is the highest category present, ranked
162
+ * by `ALERT_SEVERITY_PRIORITY`.
182
163
  * - Otherwise one badge is shown per count greater than 0, ordered most to
183
164
  * least severe and spaced by the alert-counter spacing token.
184
165
  */
@@ -193,40 +174,9 @@ export class ObcTreeNavigationItem extends LitElement {
193
174
  this.wrapperElement?.focus(options);
194
175
  }
195
176
 
196
- /**
197
- * The badge(s) to render from `alerts`, as `{type, count}` pairs already in
198
- * severity order, ranked by `ALERT_SEVERITY_PRIORITY`.
199
- *
200
- * - No `alerts`, or every count 0 → no badges.
201
- * - `combine` → a single pair: the summed count typed as the highest
202
- * category that has any alerts.
203
- * - Otherwise → one pair per count greater than 0.
204
- */
205
- private get alertBadges(): {type: AlertType; count: number}[] {
206
- const alerts = this.alerts;
207
- if (!alerts) return [];
208
- const countByType: Partial<Record<AlertType, number>> = {
209
- [AlertType.LevelCritical]: alerts.countLevelCritical ?? 0,
210
- [AlertType.Alarm]: alerts.countAlarm ?? 0,
211
- [AlertType.LevelHigh]: alerts.countLevelHigh ?? 0,
212
- [AlertType.Warning]: alerts.countWarning ?? 0,
213
- [AlertType.LevelMedium]: alerts.countLevelMedium ?? 0,
214
- [AlertType.Caution]: alerts.countCaution ?? 0,
215
- [AlertType.LevelLow]: alerts.countLevelLow ?? 0,
216
- [AlertType.LevelDiagnostic]: alerts.countLevelDiagnostic ?? 0,
217
- };
218
- // Order (and, when combining, rank) by the shared severity priority,
219
- // keeping only the severities this component exposes.
220
- const ranked = ALERT_SEVERITY_PRIORITY.filter(
221
- (type) => type in countByType
222
- ).map((type) => ({type, count: countByType[type] ?? 0}));
223
- if (alerts.combine) {
224
- const total = ranked.reduce((sum, b) => sum + b.count, 0);
225
- const highest = ranked.find((b) => b.count > 0);
226
- if (!highest) return [];
227
- return [{type: highest.type, count: total}];
228
- }
229
- return ranked.filter((b) => b.count > 0);
177
+ /** The badge(s) to render from `alerts`, ranked by `rankAlertCounts`. */
178
+ private get alertBadges(): RankedAlertCount[] {
179
+ return this.alerts ? rankAlertCounts(this.alerts, this.alerts.combine) : [];
230
180
  }
231
181
 
232
182
  /** A root-level row has no ancestor columns, so it draws no connector lines. */
@@ -13,10 +13,20 @@
13
13
  's8119af112f965c78': `Brillantez`,
14
14
  'sa7f8e003ff63ef9f': `No alertas activas`,
15
15
  'se0955919920ee87d': `Día`,
16
+ 'scd93bdcb7863611a': `Alarm`,
17
+ 's34be76c6b1eadbef': `Warning`,
18
+ 'sf11564cc75a8d5a6': `Caution`,
19
+ 'sa45f3300b97dabe8': `Critical`,
20
+ 's9deeefd89ca8a81d': `High`,
21
+ 'sac6bdc71f750ac96': `Medium`,
22
+ 's24f3a319b88552c1': `Low`,
23
+ 'sca9e13f23f0c433e': `Diagnostic`,
24
+ 'sf268637d594dacb2': `Shelved`,
25
+ 's88d1c1e15ca1aade': `Alerts`,
26
+ 's68395a4c26a9e8ac': `Silence`,
16
27
  'sf993bb199fefbe04': `All`,
17
28
  's01b603dddbaba7b6': `Unacked`,
18
29
  'se92d11374fcdf768': `No unacknowledged alerts`,
19
- 'sf268637d594dacb2': `Shelved`,
20
30
  's2a2488656f97b746': `No shelved alerts`,
21
31
  'sf59d9197d2e50c93': `Blocked`,
22
32
  's61a9ee872da3a0cd': `No blocked alerts`,
@@ -26,8 +36,6 @@
26
36
  's5ba9818f6a39054c': `Go to the 'Alert list' for more details or to manage existing alerts.`,
27
37
  'sa45a194b58837e4f': `Active`,
28
38
  'sa6ef04345891fb38': `ACK visible`,
29
- 's68395a4c26a9e8ac': `Silence`,
30
- 's88d1c1e15ca1aade': `Alerts`,
31
39
  's3cf89cb47fdde7e9': `Link`,
32
40
  's47278872f6826da8': `Dusk`,
33
41
  's9e34794bcfdc6665': `Bright`,
@@ -41,6 +41,14 @@
41
41
  'sf993bb199fefbe04': `Kaikki`,
42
42
  'sfa32c219a0764262': `VAHVISTA`,
43
43
  'sfbc33dd48f23752c': `Tunnistetunnus`,
44
+ 'scd93bdcb7863611a': `Alarm`,
45
+ 's34be76c6b1eadbef': `Warning`,
46
+ 'sf11564cc75a8d5a6': `Caution`,
47
+ 'sa45f3300b97dabe8': `Critical`,
48
+ 's9deeefd89ca8a81d': `High`,
49
+ 'sac6bdc71f750ac96': `Medium`,
50
+ 's24f3a319b88552c1': `Low`,
51
+ 'sca9e13f23f0c433e': `Diagnostic`,
44
52
  'sa45a194b58837e4f': `Active`,
45
53
  'sc5a5b268010fc312': `Audio muted`,
46
54
  's740deeebbea89396': `Audio on`,
@@ -173,42 +173,17 @@ obc-readout-block {
173
173
  }
174
174
 
175
175
  /* ---------- Value alert overlay ----------
176
- * A pure overlay around the value reading: it reserves no space, so toggling
177
- * the frame never shifts content. The box is offset outward by
178
- * (padding − stroke/2) on each axis so the alert-frame's outline (drawn
179
- * outside its box) lands CENTRED on the 4px/2px padding line. */
176
+ * Reserves no space, so toggling the frame never shifts content. The box sits
177
+ * on the 4px/2px padding line, where obc-alert-frame centres its stroke. */
180
178
  .value-alert-overlay {
181
179
  --_readout-value-frame-padding-horizontal: 4px;
182
180
  --_readout-value-frame-padding-vertical: 2px;
183
- /* Must match obc-alert-frame's ACTUAL outline width so the stroke stays
184
- centred on the 4px/2px line. `thickness-small` is hard-coded to 2px in
185
- alert-frame.css, so use a literal 2px here; `thickness-large` is handled
186
- below. */
187
- --_readout-value-frame-stroke: 2px;
188
181
  position: absolute;
189
- inset-inline: calc(
190
- -1 *
191
- (
192
- var(--_readout-value-frame-padding-horizontal) -
193
- var(--_readout-value-frame-stroke) / 2
194
- )
195
- );
196
- inset-block: calc(
197
- -1 *
198
- (
199
- var(--_readout-value-frame-padding-vertical) -
200
- var(--_readout-value-frame-stroke) / 2
201
- )
202
- );
182
+ inset-inline: calc(-1 * var(--_readout-value-frame-padding-horizontal));
183
+ inset-block: calc(-1 * var(--_readout-value-frame-padding-vertical));
203
184
  pointer-events: none;
204
185
  }
205
186
 
206
- .value-alert-overlay.thickness-large {
207
- --_readout-value-frame-stroke: var(
208
- --global-size-spacing-border-weight-alertframe
209
- );
210
- }
211
-
212
187
  /* ---------- Meta zone (label + unit) ----------
213
188
  * `.meta` is a row hosting the optional leading icon and `.meta-labels`; the
214
189
  * inline/stacked arrangement applies to `.meta-labels` (the label/unit pair). */
@@ -851,15 +851,7 @@ export class ObcReadout extends LitElement {
851
851
  }
852
852
  const thickness = alert.thickness ?? ObcAlertFrameThickness.Small;
853
853
  return html`
854
- <div
855
- class=${classMap({
856
- 'value-alert-overlay': true,
857
- // The outward offset is thickness-dependent (see the CSS): large frames
858
- // draw a wider outline, so the box must sit further out to stay centred.
859
- 'thickness-large': thickness === ObcAlertFrameThickness.Large,
860
- })}
861
- aria-hidden="true"
862
- >
854
+ <div class="value-alert-overlay" aria-hidden="true">
863
855
  <obc-alert-frame
864
856
  part="value-alert-frame"
865
857
  .type=${alert.type ?? ObcAlertFrameType.Regular}
@@ -181,47 +181,19 @@ obc-readout-block {
181
181
  }
182
182
 
183
183
  /* ---------- Value alert overlay ----------
184
- * A pure overlay around the value reading: it reserves no space, so toggling the
185
- * frame never changes the row height or breaks cross-row column alignment. The
186
- * box is offset outward by (padding − stroke/2) on each axis so the alert-frame's
187
- * outline (drawn outside its box) lands CENTRED on the 4px/2px padding line
188
- * (half inside / half outside), matching Figma 58:10120. */
184
+ * Reserves no space, so the frame never changes the row height or the column
185
+ * alignment. The box sits on the 4px/2px padding line, where obc-alert-frame
186
+ * centres its stroke (Figma 58:10120). */
189
187
  .value-alert-overlay {
190
188
  --_obc-readout-value-frame-padding-horizontal: 4px;
191
189
  --_obc-readout-value-frame-padding-vertical: 2px;
192
- /* Must match obc-alert-frame's ACTUAL outline width so the stroke stays centred
193
- on the 4px/2px line. `thickness-small` is hard-coded to 2px in
194
- alert-frame.css (it does NOT use the themed stroke-width token), so use a
195
- literal 2px here; `thickness-large` is handled below. */
196
- --_obc-readout-value-frame-stroke: 2px;
197
190
  position: absolute;
198
- inset-inline: calc(
199
- -1 *
200
- (
201
- var(--_obc-readout-value-frame-padding-horizontal) -
202
- var(--_obc-readout-value-frame-stroke) / 2
203
- )
204
- );
205
- inset-block: calc(
206
- -1 *
207
- (
208
- var(--_obc-readout-value-frame-padding-vertical) -
209
- var(--_obc-readout-value-frame-stroke) / 2
210
- )
211
- );
191
+ inset-inline: calc(-1 * var(--_obc-readout-value-frame-padding-horizontal));
192
+ inset-block: calc(-1 * var(--_obc-readout-value-frame-padding-vertical));
212
193
  /* Display-only: never intercept clicks on a clickable row. */
213
194
  pointer-events: none;
214
195
  }
215
196
 
216
- /* thickness=large: obc-alert-frame's large outline uses the alert-frame border
217
- weight token, so the overlay's outward offset must use the same width to keep
218
- the stroke centred on the padding line. */
219
- .value-alert-overlay.thickness-large {
220
- --_obc-readout-value-frame-stroke: var(
221
- --global-size-spacing-border-weight-alertframe
222
- );
223
- }
224
-
225
197
  /* The readout building blocks (advice / setpoint / value) now render inside
226
198
  `obc-readout-block`; their layout / colour / hinted-zero / data-quality CSS
227
199
  lives there. The list-item only bridges the public colour vars (see top) and
@@ -926,15 +926,7 @@ export class ObcReadoutListItem extends LitElement {
926
926
  }
927
927
  const thickness = alert.thickness ?? ObcAlertFrameThickness.Small;
928
928
  return html`
929
- <div
930
- class=${classMap({
931
- 'value-alert-overlay': true,
932
- // The outward offset is thickness-dependent (see the CSS): large frames
933
- // draw a wider outline, so the box must sit further out to stay centred.
934
- 'thickness-large': thickness === ObcAlertFrameThickness.Large,
935
- })}
936
- aria-hidden="true"
937
- >
929
+ <div class="value-alert-overlay" aria-hidden="true">
938
930
  <obc-alert-frame
939
931
  part="value-alert-frame"
940
932
  .type=${alert.type ?? ObcAlertFrameType.Regular}
@@ -24,14 +24,14 @@ describe('installFlashing', () => {
24
24
  });
25
25
 
26
26
  it.each(FLASH_TEMPOS)(
27
- '%s runs on for its on-time, off for 400 ms, from the document origin',
27
+ '%s runs on for its on-time, then off for its off-time, from the document origin',
28
28
  (tempo) => {
29
29
  const el = host();
30
30
  const cancel = installFlashing(el, tempo);
31
31
  const [anim] = el.getAnimations();
32
32
  const effect = anim.effect as KeyframeEffect;
33
33
  const timing = effect.getTiming();
34
- expect(timing.duration).toBe(FLASH_ON_MS[tempo] + FLASH_OFF_MS);
34
+ expect(timing.duration).toBe(FLASH_ON_MS[tempo] + FLASH_OFF_MS[tempo]);
35
35
  expect(timing.iterations).toBe(Infinity);
36
36
  const frames = effect.getKeyframes();
37
37
  expect(frames).toHaveLength(3);
@@ -46,32 +46,47 @@ describe('installFlashing', () => {
46
46
  }
47
47
  );
48
48
 
49
- it('nests the cycles so every tempo dips together', () => {
49
+ it('follows the tempo table: on for 400 ms, off for 400, 1200 or 2800 ms', () => {
50
+ expect(
51
+ FLASH_TEMPOS.map((tempo) => [FLASH_ON_MS[tempo], FLASH_OFF_MS[tempo]])
52
+ ).toEqual([
53
+ [400, 400],
54
+ [400, 1200],
55
+ [400, 2800],
56
+ ]);
57
+ });
58
+
59
+ it('nests the cycles so every tempo lights up together', () => {
50
60
  expect(flashPeriodMs(FlashingSpeed.Fast)).toBe(800);
51
61
  expect(flashPeriodMs(FlashingSpeed.Slow)).toBe(1600);
52
62
  expect(flashPeriodMs(FlashingSpeed.VerySlow)).toBe(3200);
53
63
  });
54
64
 
55
- it('drives the on/off variables in step', () => {
56
- const el = host();
57
- installFlashing(el, FlashingSpeed.Fast);
58
- const [anim] = el.getAnimations();
59
- const on = () =>
60
- getComputedStyle(el).getPropertyValue(
61
- flashVariable(FlashingSpeed.Fast, 'on')
62
- );
63
- const off = () =>
64
- getComputedStyle(el).getPropertyValue(
65
- flashVariable(FlashingSpeed.Fast, 'off')
66
- );
67
- anim.pause();
68
- anim.currentTime = 100;
69
- expect(on().trim()).toBe('1');
70
- expect(off().trim()).toBe('0');
71
- anim.currentTime = 500;
72
- expect(on().trim()).toBe('0');
73
- expect(off().trim()).toBe('1');
74
- });
65
+ it.each(FLASH_TEMPOS)(
66
+ '%s drives the on/off variables in step, on phase first',
67
+ (tempo) => {
68
+ const el = host();
69
+ installFlashing(el, tempo);
70
+ const [anim] = el.getAnimations();
71
+ const read = (phase: 'on' | 'off') =>
72
+ getComputedStyle(el)
73
+ .getPropertyValue(flashVariable(tempo, phase))
74
+ .trim();
75
+ const period = flashPeriodMs(tempo);
76
+ anim.pause();
77
+ for (const [time, lit] of [
78
+ [100, true],
79
+ [500, false],
80
+ [period - 100, false],
81
+ [period + 100, true],
82
+ ] as const) {
83
+ anim.currentTime = time;
84
+ expect([time, read('on'), read('off')]).toEqual(
85
+ lit ? [time, '1', '0'] : [time, '0', '1']
86
+ );
87
+ }
88
+ }
89
+ );
75
90
 
76
91
  it('blinkingAll installs one animation per tempo and cancels them together', () => {
77
92
  const el = host();